Templates based on classes, cache custom paths

This commit is contained in:
Pablo Ferreiro 2022-01-25 17:20:11 +01:00
parent dde4185ad7
commit 6d6ec109ca
No known key found for this signature in database
GPG key ID: 41FBCE65B779FA24
26 changed files with 236 additions and 156 deletions

View file

@ -1,7 +1,11 @@
# APP_SUBDIR=/ # Subpath your app is running, defaults to / # APP_SUBDIR=/ # Subpath your app is running, leave commented for /
# APP_CACHE=redis # Cache engine for TikTok Api, (more info on README)
# Redis, used on Helpers\CahceEngines\RedisCache (CHOOSE ONE) # LATTE_CACHE=/tmp/proxitok_api # Path for Latte cache, leave commented for ./cache/latte
# API_CACHE=redis # Cache engine for TikTok Api, (more info on README)
# REDIS_URL=redis://:password@host:port # If using password # Redis cache, used on Helpers\CacheEngines\RedisCache (CHOOSE ONE)
# REDIS_URL=redis://host:port # If not using password # API_CACHE_REDIS_URL=redis://:password@host:port # If using password
# API_CACHE_REDIS_URL=redis://host:port # If not using password
# JSON cache, used on Helpers\CacheEngines\JSONCache
# API_CACHE_JSON=/tmp/proxitok_api # Path for JSON API Cache, leave commented for ./cache/api

4
.gitignore vendored
View file

@ -2,7 +2,7 @@ node_modules
/.env /.env
/.vscode /.vscode
/vendor /vendor
/cache/views/* /cache/latte/*
!/cache/views/.gitkeep !/cache/latte/.gitkeep
/cache/api/* /cache/api/*
!/cache/api/.gitkeep !/cache/api/.gitkeep

View file

@ -1,7 +1,8 @@
<footer class="footer"> <footer class="footer">
<div class="content"> <div class="content has-text-centered">
<p class="has-text-centered"> <p>
Made with <span style="color: #e25555;">&#9829;</span> in <a href="https://github.com/pablouser1/ProxiTok">Github</a> Made with <span style="color: #e25555;">&#9829;</span> in <a href="https://github.com/pablouser1/ProxiTok">Github</a>
</p> </p>
<p>Version: {version()}</p>
</div> </div>
</footer> </footer>

View file

@ -1,7 +1,7 @@
{ {
"name": "pablouser1/proxitok", "name": "pablouser1/proxitok",
"description": "An alternative frontend for TikTok", "description": "An alternative frontend for TikTok",
"version": "1.1.2", "version": "1.2.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"repositories": [ "repositories": [
{ {
@ -18,7 +18,8 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Helpers\\": "helpers/" "Helpers\\": "helpers/",
"Views\\Models\\": "views/models/"
} }
} }
} }

View file

@ -2,23 +2,30 @@
namespace Helpers\CacheEngines; namespace Helpers\CacheEngines;
class JSONCache { class JSONCache {
const CACHE_PATH = __DIR__ . '/../../cache/api/'; private string $cache_path = __DIR__ . '/../../cache/api';
function __construct() {
if (isset($_ENV['API_CACHE_JSON']) && !empty($_ENV['API_CACHE_JSON'])) {
$this->cache_path = $_ENV['API_CACHE_JSON'];
}
}
public function get(string $cache_key): object|false { public function get(string $cache_key): object|false {
if (is_file(self::CACHE_PATH . $cache_key . '.json')) { $filename = $this->cache_path . '/' . $cache_key . '.json';
if (is_file($filename)) {
$time = time(); $time = time();
$json_string = file_get_contents(self::CACHE_PATH . $cache_key . '.json'); $json_string = file_get_contents($filename);
$element = json_decode($json_string); $element = json_decode($json_string);
if ($time < $element->expires) { if ($time < $element->expires) {
return $element->data; return $element->data;
} }
// Remove file if expired // Remove file if expired
unlink(self::CACHE_PATH . $cache_key . '.json'); unlink($filename);
} }
return false; return false;
} }
public function set(string $cache_key, mixed $data, $timeout = 3600) { public function set(string $cache_key, mixed $data, $timeout = 3600) {
file_put_contents(self::CACHE_PATH . $cache_key . '.json', json_encode([ file_put_contents($this->cache_path . '/' . $cache_key . '.json', json_encode([
'data' => $data, 'data' => $data,
'expires' => time() + $timeout 'expires' => time() + $timeout
])); ]));

View file

@ -25,8 +25,8 @@ class Misc {
} }
} }
// Cache config // Cache config
if (isset($_ENV['APP_CACHE'])) { if (isset($_ENV['API_CACHE'])) {
switch ($_ENV['APP_CACHE']) { switch ($_ENV['API_CACHE']) {
case 'json': case 'json':
$cacheEngine = new JSONCache(); $cacheEngine = new JSONCache();
break; break;
@ -54,15 +54,24 @@ class Misc {
$subdir = ''; $subdir = '';
} }
$latte = new \Latte\Engine; $latte = new \Latte\Engine;
$latte->setTempDirectory(__DIR__ . '/../cache/views'); $cache_path = isset($_ENV['LATTE_CACHE']) && !empty($_ENV['LATTE_CACHE']) ? $_ENV['LATTE_CACHE'] : __DIR__ . '/../cache/latte';
$latte->setTempDirectory($cache_path);
// -- CUSTOM FUNCTIONS -- //
// Import assets
$latte->addFunction('assets', function (string $name, string $type) use ($subdir) { $latte->addFunction('assets', function (string $name, string $type) use ($subdir) {
$path = "{$subdir}/{$type}/{$name}"; $path = "{$subdir}/{$type}/{$name}";
return $path; return $path;
}); });
// Relative path
$latte->addFunction('path', function (string $name) use ($subdir) { $latte->addFunction('path', function (string $name) use ($subdir) {
$path = "{$subdir}/{$name}"; $path = "{$subdir}/{$name}";
return $path; return $path;
}); });
// Version being used
$latte->addFunction('version', function () {
return \Composer\InstalledVersions::getVersion('pablouser1/proxitok');
});
// https://stackoverflow.com/a/36365553 // https://stackoverflow.com/a/36365553
$latte->addFunction('number', function (int $x) { $latte->addFunction('number', function (int $x) {
if($x > 1000) { if($x > 1000) {

View file

@ -2,6 +2,7 @@
use Helpers\Following; use Helpers\Following;
use Helpers\Misc; use Helpers\Misc;
use Steampixel\Route; use Steampixel\Route;
use Views\Models\FollowingTemplate;
// Showing // Showing
Route::add('/following', function () { Route::add('/following', function () {
@ -28,9 +29,5 @@ Route::add('/following', function () {
'hasMore' => false 'hasMore' => false
]; ];
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('following'), [ $latte->render(Misc::getView('following'), new FollowingTemplate($following, $feed));
'following' => $following,
'feed' => $feed,
'title' => 'Following'
]);
}); });

View file

@ -2,18 +2,22 @@
require __DIR__ . '/assets.php'; require __DIR__ . '/assets.php';
require __DIR__ . '/settings.php'; require __DIR__ . '/settings.php';
require __DIR__ . '/following.php'; require __DIR__ . '/following.php';
use Steampixel\Route; use Steampixel\Route;
use Helpers\Misc; use Helpers\Misc;
use Helpers\Error; use Helpers\Error;
use Views\Models\BaseTemplate;
use Views\Models\FeedTemplate;
use Views\Models\ItemTemplate;
Route::add('/', function () { Route::add('/', function () {
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('home'), ['title' => 'Home']); $latte->render(Misc::getView('home'), new BaseTemplate('Home'));
}); });
Route::add('/about', function () { Route::add('/about', function () {
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('about'), ['title' => 'About']); $latte->render(Misc::getView('about'), new BaseTemplate('About'));
}); });
Route::add("/trending", function () { Route::add("/trending", function () {
@ -25,10 +29,7 @@ Route::add("/trending", function () {
$feed = $api->getTrendingFeed($cursor); $feed = $api->getTrendingFeed($cursor);
if ($feed->meta->success) { if ($feed->meta->success) {
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('trending'), [ $latte->render(Misc::getView('trending'), new FeedTemplate('Trending', $feed));
'feed' => $feed,
'title' => 'Trending'
]);
} else { } else {
Error::show($feed->meta); Error::show($feed->meta);
} }
@ -47,10 +48,7 @@ Route::add("/@([^/]+)", function (string $username) {
return 'Private account detected! Not supported'; return 'Private account detected! Not supported';
} }
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('user'), [ $latte->render(Misc::getView('user'), new FeedTemplate($feed->info->detail->user->nickname, $feed));
'feed' => $feed,
'title' => $feed->info->detail->user->nickname
]);
} else { } else {
Error::show($feed->meta); Error::show($feed->meta);
} }
@ -61,10 +59,7 @@ Route::add('/video/([^/]+)', function (string $video_id) {
$item = $api->getVideoByID($video_id); $item = $api->getVideoByID($video_id);
if ($item->meta->success) { if ($item->meta->success) {
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('video'), [ $latte->render(Misc::getView('video'), new ItemTemplate($item->info->detail->user->nickname, $item));
'item' => $item,
'title' => $item->info->detail->user->nickname
]);
} else { } else {
Error::show($item->meta); Error::show($item->meta);
} }
@ -80,10 +75,7 @@ Route::add('/music/([^/]+)', function (string $music_id) {
$feed = $api->getMusicFeed($music_id, $cursor); $feed = $api->getMusicFeed($music_id, $cursor);
if ($feed->meta->success) { if ($feed->meta->success) {
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('music'), [ $latte->render(Misc::getView('music'), new FeedTemplate('Music', $feed));
'feed' => $feed,
'title' => 'Music'
]);
} else { } else {
Error::show($feed->meta); Error::show($feed->meta);
} }
@ -98,10 +90,7 @@ Route::add('/tag/(\w+)', function (string $name) {
$feed = $api->getChallengeFeed($name, $cursor); $feed = $api->getChallengeFeed($name, $cursor);
if ($feed->meta->success) { if ($feed->meta->success) {
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('tag'), [ $latte->render(Misc::getView('tag'), new FeedTemplate('Tag', $feed));
'feed' => $feed,
'title' => 'Tag'
]);
} else { } else {
Error::show($feed->meta); Error::show($feed->meta);
} }

View file

@ -3,15 +3,12 @@
use Helpers\Following; use Helpers\Following;
use Helpers\Settings; use Helpers\Settings;
use Helpers\Misc; use Helpers\Misc;
use Views\Models\SettingsTemplate;
use Steampixel\Route; use Steampixel\Route;
Route::add("/settings", function () { Route::add("/settings", function () {
$latte = Misc::latte(); $latte = Misc::latte();
$latte->render(Misc::getView('settings'), [ $latte->render(Misc::getView('settings'), new SettingsTemplate());
"proxy_elements" => Settings::PROXY,
"following" => Following::get(),
"title" => "Settings"
]);
}); });
Route::add("/settings/proxy", function () { Route::add("/settings/proxy", function () {

View file

@ -33,9 +33,9 @@ bulmaswatch@^0.8.1:
integrity sha512-7HGm5v9If6gzxbTht4/oVS0dhySp6g/JyTrxmpSXHXgDQXivvxiuVmcJOZo3PFv9GAOn4om7SK36I2V8W81sgw== integrity sha512-7HGm5v9If6gzxbTht4/oVS0dhySp6g/JyTrxmpSXHXgDQXivvxiuVmcJOZo3PFv9GAOn4om7SK36I2V8W81sgw==
"chokidar@>=3.0.0 <4.0.0": "chokidar@>=3.0.0 <4.0.0":
version "3.5.2" version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies: dependencies:
anymatch "~3.1.2" anymatch "~3.1.2"
braces "~3.0.2" braces "~3.0.2"
@ -113,18 +113,18 @@ readdirp@~3.6.0:
picomatch "^2.2.1" picomatch "^2.2.1"
sass@^1.46.0: sass@^1.46.0:
version "1.48.0" version "1.49.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.48.0.tgz#b53cfccc1b8ab4be375cc54f306fda9d4711162c" resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078"
integrity sha512-hQi5g4DcfjcipotoHZ80l7GNJHGqQS5LwMBjVYB/TaT0vcSSpbgM8Ad7cgfsB2M0MinbkEQQPO9+sjjSiwxqmw== integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==
dependencies: dependencies:
chokidar ">=3.0.0 <4.0.0" chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0" immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0" source-map-js ">=0.6.2 <2.0.0"
"source-map-js@>=0.6.2 <2.0.0": "source-map-js@>=0.6.2 <2.0.0":
version "1.0.1" version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
to-regex-range@^5.0.1: to-regex-range@^5.0.1:
version "5.0.1" version "5.0.1"

View file

@ -1,7 +1,7 @@
{layout '../layouts/default.latte'} {layout '../layouts/default.latte'}
{block header} {block header}
<p class="title">About</p> <p class="title">About</p>
{/block} {/block}
{block content} {block content}

View file

@ -1,9 +1,9 @@
{layout '../layouts/hero.latte'} {layout '../layouts/hero.latte'}
{block content} {block content}
<p class="title">There was an error processing your request!</p> <p class="title">There was an error processing your request!</p>
<p class="subtitle">HTTP Code: {$error->http_code}</p> <p class="subtitle">HTTP Code: {$error->http_code}</p>
{if $error->tiktok_code} {if $error->tiktok_code}
<p class="subtitle">API error code {$error->tiktok_code} ({$error->tiktok_msg})</p> <p class="subtitle">API error code {$error->tiktok_code} ({$error->tiktok_msg})</p>
{/if} {/if}
{/block} {/block}

View file

@ -1,7 +1,7 @@
{layout '../layouts/default.latte'} {layout '../layouts/default.latte'}
{block header} {block header}
<p class="title">Following</p> <p class="title">Following</p>
{/block} {/block}
{block content} {block content}

View file

@ -1,58 +1,58 @@
{layout '../layouts/hero.latte'} {layout '../layouts/hero.latte'}
{block content} {block content}
<p class="title">Welcome to ProxiTok!</p> <p class="title">Welcome to ProxiTok!</p>
<p class="subtitle">An alternative open source frontend for TikTok</p> <p class="subtitle">An alternative open source frontend for TikTok</p>
<p>Search user:</p> <p>Search user:</p>
<form id="username_form"> <form id="username_form">
<div class="field has-addons has-addons-centered"> <div class="field has-addons has-addons-centered">
<div class="control"> <div class="control">
<input name="username" class="input" type="text" placeholder="Type username" required /> <input name="username" class="input" type="text" placeholder="Type username" required />
</div> </div>
<div class="control"> <div class="control">
<button class="button is-success" type="submit">Go</button> <button class="button is-success" type="submit">Go</button>
</div> </div>
</div> </div>
</form> </form>
<hr /> <hr />
<p>Search video by id:</p> <p>Search video by id:</p>
<form id="video_form"> <form id="video_form">
<div class="field has-addons has-addons-centered"> <div class="field has-addons has-addons-centered">
<div class="control"> <div class="control">
<input name="video_id" class="input" type="text" placeholder="Type video ID" required /> <input name="video_id" class="input" type="text" placeholder="Type video ID" required />
</div> </div>
<div class="control"> <div class="control">
<button class="button is-success" type="submit">Go</button> <button class="button is-success" type="submit">Go</button>
</div> </div>
</div> </div>
</form> </form>
<hr /> <hr />
<p>Search tag:</p> <p>Search tag:</p>
<form id="tag_form"> <form id="tag_form">
<div class="field has-addons has-addons-centered"> <div class="field has-addons has-addons-centered">
<div class="control"> <div class="control">
<input name="tag" class="input" type="text" placeholder="Type tag" required /> <input name="tag" class="input" type="text" placeholder="Type tag" required />
</div> </div>
<div class="control"> <div class="control">
<button class="button is-success" type="submit">Go</button> <button class="button is-success" type="submit">Go</button>
</div> </div>
</div> </div>
</form> </form>
<hr /> <hr />
<p>Search music videos by id:</p> <p>Search music videos by id:</p>
<form id="music_form"> <form id="music_form">
<div class="field has-addons has-addons-centered"> <div class="field has-addons has-addons-centered">
<div class="control"> <div class="control">
<input name="music_id" class="input" type="text" placeholder="Type music id" required /> <input name="music_id" class="input" type="text" placeholder="Type music id" required />
</div> </div>
<div class="control"> <div class="control">
<button class="button is-success" type="submit">Go</button> <button class="button is-success" type="submit">Go</button>
</div> </div>
</div> </div>
</form> </form>
<hr /> <hr />
<p>Trending:</p> <p>Trending:</p>
<a class="button is-success" href="./trending">Go</a> <a class="button is-success" href="./trending">Go</a>
{/block} {/block}
{block extra} {block extra}

View file

@ -0,0 +1,14 @@
<?php
namespace Views\Models;
/**
* Base for all templates, needs a Title to set
*/
class BaseTemplate {
public string $title;
public string $version;
function __construct(string $title) {
$this->title = $title;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Views\Models;
/**
* Base for templates with a feed
*/
class FeedTemplate extends BaseTemplate {
public object $feed;
function __construct(string $title, object $feed) {
parent::__construct($title);
$this->feed = $feed;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Views\Models;
/**
* Exclusive for /following
*/
class FollowingTemplate extends FeedTemplate {
public array $following;
function __construct(array $following, object $feed) {
parent::__construct('Following', $feed);
$this->following = $following;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Views\Models;
/**
* Base for templates with item (only /video at the time)
*/
class ItemTemplate extends BaseTemplate {
public object $item;
function __construct(string $title, object $item) {
parent::__construct($title);
$this->item = $item;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Views\Models;
use \Helpers\Following;
use \Helpers\Settings;
/**
* Exclusive for /settings
*/
class SettingsTemplate extends BaseTemplate {
public array $proxy_elements = [];
public array $following = [];
function __construct() {
parent::__construct('Settings');
$this->proxy_elements = Settings::PROXY;
$this->following = Following::get();
}
}

View file

@ -1,9 +1,9 @@
{layout '../layouts/default.latte'} {layout '../layouts/default.latte'}
{block header} {block header}
<p class="title">{$feed->info->detail->music->title}</p> <p class="title">{$feed->info->detail->music->title}</p>
<p class="subtitle">{$feed->info->detail->music->desc}</p> <p class="subtitle">{$feed->info->detail->music->desc}</p>
<p>Videos: {number($feed->info->detail->stats->videoCount)}</p> <p>Videos: {number($feed->info->detail->stats->videoCount)}</p>
{/block} {/block}
{block content} {block content}

View file

@ -1,15 +1,15 @@
{layout '../layouts/default.latte'} {layout '../layouts/default.latte'}
{block header} {block header}
<p class="title">Settings</p> <p class="title">Settings</p>
{/block} {/block}
{block content} {block content}
<!-- Proxy settings --> <!-- Proxy settings -->
<p class="title">Proxy</p> <p class="title">Proxy</p>
{include '../components/settings/proxy.latte'} {include '../components/settings/proxy.latte'}
<hr /> <hr />
<!-- Following --> <!-- Following -->
<p class="title">Following</p> <p class="title">Following</p>
{include '../components/settings/following.latte'} {include '../components/settings/following.latte'}
{/block} {/block}

View file

@ -1,9 +1,9 @@
{layout '../layouts/default.latte'} {layout '../layouts/default.latte'}
{block header} {block header}
<p class="title">{$feed->info->detail->challenge->title}</p> <p class="title">{$feed->info->detail->challenge->title}</p>
<p class="subtitle">{$feed->info->detail->challenge->desc}</p> <p class="subtitle">{$feed->info->detail->challenge->desc}</p>
<p>Videos: {number($feed->info->detail->stats->videoCount)} / Views: {number($feed->info->detail->stats->viewCount)}</p> <p>Videos: {number($feed->info->detail->stats->videoCount)} / Views: {number($feed->info->detail->stats->viewCount)}</p>
{/block} {/block}
{block content} {block content}

View file

@ -1,7 +1,7 @@
{layout '../layouts/default.latte'} {layout '../layouts/default.latte'}
{block header} {block header}
<p class="title">Trending</p> <p class="title">Trending</p>
{/block} {/block}
{block content} {block content}

View file

@ -1,13 +1,13 @@
{layout '../layouts/default.latte'} {layout '../layouts/default.latte'}
{block header} {block header}
<figure class="figure is-96x96"> <figure class="figure is-96x96">
<img src="{path('stream?url=' . urlencode($feed->info->detail->user->avatarThumb))}" /> <img src="{path('stream?url=' . urlencode($feed->info->detail->user->avatarThumb))}" />
</figure> </figure>
<p class="title">{$feed->info->detail->user->uniqueId}'s profile</p> <p class="title">{$feed->info->detail->user->uniqueId}'s profile</p>
<p class="subtitle">{$feed->info->detail->user->signature}</p> <p class="subtitle">{$feed->info->detail->user->signature}</p>
<p>Following: {number($feed->info->detail->stats->followingCount)} / Followers: {number($feed->info->detail->stats->followerCount)}</p> <p>Following: {number($feed->info->detail->stats->followingCount)} / Followers: {number($feed->info->detail->stats->followerCount)}</p>
<p>Hearts: {number($feed->info->detail->stats->heartCount)} / Videos: {$feed->info->detail->stats->videoCount}</p> <p>Hearts: {number($feed->info->detail->stats->heartCount)} / Videos: {$feed->info->detail->stats->videoCount}</p>
{/block} {/block}
{block content} {block content}

View file

@ -1,21 +1,21 @@
{layout '../layouts/hero.latte'} {layout '../layouts/hero.latte'}
{block content} {block content}
<div class="columns is-centered is-vcentered"> <div class="columns is-centered is-vcentered">
<div class="column"> <div class="column">
<video autoplay controls src="{path('stream?url=' . urlencode($item->items[0]->video->playAddr))}"></video> <video autoplay controls src="{path('stream?url=' . urlencode($item->items[0]->video->playAddr))}"></video>
</div> </div>
<div class="column has-text-centered"> <div class="column has-text-centered">
<div class="box"> <div class="box">
<p class="title">Video by <a href="{path('@'.$item->info->detail->user->uniqueId)}">{$item->info->detail->user->uniqueId}</a></p> <p class="title">Video by <a href="{path('@'.$item->info->detail->user->uniqueId)}">{$item->info->detail->user->uniqueId}</a></p>
<p class="subtitle">{$item->items[0]->desc}</p> <p class="subtitle">{$item->items[0]->desc}</p>
<p>Played {number($item->info->detail->stats->playCount)} times</p> <p>Played {number($item->info->detail->stats->playCount)} times</p>
<p>Shared {number($item->info->detail->stats->shareCount)} times / {number($item->info->detail->stats->commentCount)} comments</p> <p>Shared {number($item->info->detail->stats->shareCount)} times / {number($item->info->detail->stats->commentCount)} comments</p>
<hr /> <hr />
<a href="{path('stream?url=' . urlencode($item->items[0]->video->playAddr) . '&download=1')}" class="button is-info">Download video</a> <a href="{path('stream?url=' . urlencode($item->items[0]->video->playAddr) . '&download=1')}" class="button is-info">Download video</a>
<p>{$item->items[0]->music->title}</p> <p>{$item->items[0]->music->title}</p>
<audio src="{path('stream?url=' . urlencode($item->items[0]->music->playUrl))}" controls preload="none"></audio> <audio src="{path('stream?url=' . urlencode($item->items[0]->music->playUrl))}" controls preload="none"></audio>
</div>
</div> </div>
</div> </div>
</div>
{/block} {/block}