mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 06:21:27 +00:00
Update Portfolios, add ActivityPub + RSS support, light mode, style customization and more
This commit is contained in:
parent
4e1d0ed596
commit
5ad0d8834d
9 changed files with 734 additions and 97 deletions
|
@ -13,6 +13,10 @@ use App\Services\StatusService;
|
||||||
|
|
||||||
class PortfolioController extends Controller
|
class PortfolioController extends Controller
|
||||||
{
|
{
|
||||||
|
const RSS_FEED_KEY = 'pf:portfolio:rss-feed:';
|
||||||
|
const CACHED_FEED_KEY = 'pf:portfolio:cached-feed:';
|
||||||
|
const RECENT_FEED_KEY = 'pf:portfolio:recent-feed:';
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
return view('portfolio.index');
|
return view('portfolio.index');
|
||||||
|
@ -60,11 +64,11 @@ class PortfolioController extends Controller
|
||||||
$user = AccountService::get($post['account']['id']);
|
$user = AccountService::get($post['account']['id']);
|
||||||
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
||||||
|
|
||||||
if($user['locked'] || $portfolio->active != true) {
|
if(!$portfolio || $user['locked'] || $portfolio->active != true) {
|
||||||
return view('portfolio.404');
|
return view('portfolio.404');
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!$post || $post['visibility'] != 'public' || $post['pf_type'] != 'photo' || $user['id'] != $post['account']['id']) {
|
if(!$post || $post['visibility'] != 'public' || !in_array($post['pf_type'], ['photo', 'photo:album']) || $user['id'] != $post['account']['id']) {
|
||||||
return view('portfolio.404');
|
return view('portfolio.404');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +121,7 @@ class PortfolioController extends Controller
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'profile_source' => 'required|in:recent,custom',
|
'profile_source' => 'required|in:recent,custom',
|
||||||
'layout' => 'required|in:grid,masonry',
|
'layout' => 'required|in:grid,masonry',
|
||||||
'layout_container' => 'required|in:fixed,fluid'
|
'layout_container' => 'required|in:fixed,fluid',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
||||||
|
@ -140,6 +144,7 @@ class PortfolioController extends Controller
|
||||||
$portfolio->show_bio = $request->input('show_bio') === 'on';
|
$portfolio->show_bio = $request->input('show_bio') === 'on';
|
||||||
$portfolio->profile_layout = $request->input('layout');
|
$portfolio->profile_layout = $request->input('layout');
|
||||||
$portfolio->profile_container = $request->input('layout_container');
|
$portfolio->profile_container = $request->input('layout_container');
|
||||||
|
$portfolio->metadata = $metadata;
|
||||||
$portfolio->save();
|
$portfolio->save();
|
||||||
|
|
||||||
return redirect('/' . $request->user()->username);
|
return redirect('/' . $request->user()->username);
|
||||||
|
@ -171,16 +176,24 @@ class PortfolioController extends Controller
|
||||||
return response()->json([], 400);
|
return response()->json([], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
return collect($portfolio->metadata['posts'])->map(function($p) {
|
$feed = Cache::remember(self::CACHED_FEED_KEY . $portfolio->profile_id, 86400, function() use($portfolio) {
|
||||||
return StatusService::get($p);
|
return collect($portfolio->metadata['posts'])->map(function($p) {
|
||||||
})
|
return StatusService::get($p);
|
||||||
->filter(function($p) {
|
})
|
||||||
return $p && isset($p['account']);
|
->filter(function($p) {
|
||||||
})->values();
|
return $p && isset($p['account']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if($portfolio->metadata && isset($portfolio->metadata['feed_order']) && $portfolio->metadata['feed_order'] === 'recent') {
|
||||||
|
return $feed->reverse()->values();
|
||||||
|
} else {
|
||||||
|
return $feed->values();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getRecentFeed($id) {
|
protected function getRecentFeed($id) {
|
||||||
$media = Cache::remember('portfolio:recent-feed:' . $id, 3600, function() use($id) {
|
$media = Cache::remember(self::RECENT_FEED_KEY . $id, 3600, function() use($id) {
|
||||||
return DB::table('media')
|
return DB::table('media')
|
||||||
->whereProfileId($id)
|
->whereProfileId($id)
|
||||||
->whereNotNull('status_id')
|
->whereNotNull('status_id')
|
||||||
|
@ -215,6 +228,14 @@ class PortfolioController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
return $res->map(function($p) {
|
return $res->map(function($p) {
|
||||||
|
$metadata = $p->metadata;
|
||||||
|
$bgColor = $metadata && isset($metadata['background_color']) ? $metadata['background_color'] : '#000000';
|
||||||
|
$textColor = $metadata && isset($metadata['text_color']) ? $metadata['text_color'] : '#d4d4d8';
|
||||||
|
$rssEnabled = $metadata && isset($metadata['rss_enabled']) ? $metadata['rss_enabled'] : false;
|
||||||
|
$rssButton = $metadata && isset($metadata['show_rss_button']) ? $metadata['show_rss_button'] : false;
|
||||||
|
$colorScheme = $metadata && isset($metadata['color_scheme']) ? $metadata['color_scheme'] : 'dark';
|
||||||
|
$feedOrder = $metadata && isset($metadata['feed_order']) ? $metadata['feed_order'] : 'oldest';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'url' => $p->url(),
|
'url' => $p->url(),
|
||||||
'pid' => (string) $p->profile_id,
|
'pid' => (string) $p->profile_id,
|
||||||
|
@ -228,6 +249,13 @@ class PortfolioController extends Controller
|
||||||
'show_bio' => (bool) $p->show_bio,
|
'show_bio' => (bool) $p->show_bio,
|
||||||
'profile_layout' => $p->profile_layout,
|
'profile_layout' => $p->profile_layout,
|
||||||
'profile_source' => $p->profile_source,
|
'profile_source' => $p->profile_source,
|
||||||
|
'color_scheme' => $colorScheme,
|
||||||
|
'background_color' => $bgColor,
|
||||||
|
'text_color' => $textColor,
|
||||||
|
'show_profile_button' => true,
|
||||||
|
'rss_enabled' => $rssEnabled,
|
||||||
|
'show_rss_button' => $rssButton,
|
||||||
|
'feed_order' => $feedOrder,
|
||||||
'metadata' => $p->metadata
|
'metadata' => $p->metadata
|
||||||
];
|
];
|
||||||
})->first();
|
})->first();
|
||||||
|
@ -248,8 +276,13 @@ class PortfolioController extends Controller
|
||||||
if(!$p) {
|
if(!$p) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
$metadata = $p->metadata;
|
||||||
|
|
||||||
return [
|
$rssEnabled = $metadata && isset($metadata['rss_enabled']) ? $metadata['rss_enabled'] : false;
|
||||||
|
$rssButton = $metadata && isset($metadata['show_rss_button']) ? $metadata['show_rss_button'] : false;
|
||||||
|
$profileButton = $metadata && isset($metadata['show_profile_button']) ? $metadata['show_profile_button'] : false;
|
||||||
|
|
||||||
|
$res = [
|
||||||
'url' => $p->url(),
|
'url' => $p->url(),
|
||||||
'show_captions' => (bool) $p->show_captions,
|
'show_captions' => (bool) $p->show_captions,
|
||||||
'show_license' => (bool) $p->show_license,
|
'show_license' => (bool) $p->show_license,
|
||||||
|
@ -259,8 +292,27 @@ class PortfolioController extends Controller
|
||||||
'show_avatar' => (bool) $p->show_avatar,
|
'show_avatar' => (bool) $p->show_avatar,
|
||||||
'show_bio' => (bool) $p->show_bio,
|
'show_bio' => (bool) $p->show_bio,
|
||||||
'profile_layout' => $p->profile_layout,
|
'profile_layout' => $p->profile_layout,
|
||||||
'profile_source' => $p->profile_source
|
'profile_source' => $p->profile_source,
|
||||||
|
'show_profile_button' => $profileButton,
|
||||||
|
'rss_enabled' => $rssEnabled,
|
||||||
|
'show_rss_button' => $rssButton,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if($rssEnabled) {
|
||||||
|
$res['rss_feed_url'] = $p->permalink('.rss');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($p->metadata) {
|
||||||
|
if(isset($p->metadata['background_color'])) {
|
||||||
|
$res['background_color'] = $p->metadata['background_color'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($p->metadata['text_color'])) {
|
||||||
|
$res['text_color'] = $p->metadata['text_color'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function storeSettings(Request $request)
|
public function storeSettings(Request $request)
|
||||||
|
@ -268,11 +320,99 @@ class PortfolioController extends Controller
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'profile_layout' => 'sometimes|in:grid,masonry,album'
|
'active' => 'sometimes|boolean',
|
||||||
|
'show_captions' => 'sometimes|boolean',
|
||||||
|
'show_license' => 'sometimes|boolean',
|
||||||
|
'show_location' => 'sometimes|boolean',
|
||||||
|
'show_timestamp' => 'sometimes|boolean',
|
||||||
|
'show_link' => 'sometimes|boolean',
|
||||||
|
'show_avatar' => 'sometimes|boolean',
|
||||||
|
'show_bio' => 'sometimes|boolean',
|
||||||
|
'profile_layout' => 'sometimes|in:grid,masonry,album',
|
||||||
|
'profile_source' => 'sometimes|in:recent,custom',
|
||||||
|
'color_scheme' => 'sometimes|in:light,dark,custom',
|
||||||
|
'show_profile_button' => 'sometimes|boolean',
|
||||||
|
'rss_enabled' => 'sometimes|boolean',
|
||||||
|
'show_rss_button' => 'sometimes|boolean',
|
||||||
|
'feed_order' => 'sometimes|in:oldest,recent',
|
||||||
|
'background_color' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
|
||||||
|
],
|
||||||
|
'text_color' => [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$res = Portfolio::whereUserId($request->user()->id)
|
$res = Portfolio::whereUserId($request->user()->id)->firstOrFail();
|
||||||
->update($request->only([
|
$pid = $request->user()->profile_id;
|
||||||
|
$metadata = $res->metadata;
|
||||||
|
$clearFeedCache = false;
|
||||||
|
|
||||||
|
if($request->has('color_scheme')) {
|
||||||
|
$metadata['color_scheme'] = $request->input('color_scheme');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('background_color')) {
|
||||||
|
$metadata['background_color'] = $request->input('background_color');
|
||||||
|
$bgc = $request->background_color;
|
||||||
|
if($bgc && $bgc !== '#000000') {
|
||||||
|
$metadata['color_scheme'] = 'custom';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('text_color')) {
|
||||||
|
$metadata['text_color'] = $request->input('text_color');
|
||||||
|
$txc = $request->text_color;
|
||||||
|
if($txc && $txc !== '#d4d4d8') {
|
||||||
|
$metadata['color_scheme'] = 'custom';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('show_profile_button')) {
|
||||||
|
$metadata['show_profile_button'] = $request->input('show_profile_button');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('rss_enabled')) {
|
||||||
|
$metadata['rss_enabled'] = $request->input('rss_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('show_rss_button')) {
|
||||||
|
$metadata['show_rss_button'] = $metadata['rss_enabled'] ? $request->input('show_rss_button') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('feed_order')) {
|
||||||
|
$metadata['feed_order'] = $request->input('feed_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($metadata['background_color']) || isset($metadata['text_color'])) {
|
||||||
|
$bgc = isset($metadata['background_color']) ? $metadata['background_color'] : null;
|
||||||
|
$txc = isset($metadata['text_color']) ? $metadata['text_color'] : null;
|
||||||
|
|
||||||
|
if((!$bgc || $bgc == '#000000') && (!$txc || $txc === '#d4d4d8') && $request->color_scheme != 'light') {
|
||||||
|
$metadata['color_scheme'] = 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->has('color_scheme') && $request->color_scheme === 'light') {
|
||||||
|
$metadata['background_color'] = '#ffffff';
|
||||||
|
$metadata['text_color'] = '#000000';
|
||||||
|
$metadata['color_scheme'] = 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->metadata !== $metadata) {
|
||||||
|
$res->metadata = $metadata;
|
||||||
|
$res->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->profile_layout != $res->profile_layout) {
|
||||||
|
$clearFeedCache = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$res->update($request->only([
|
||||||
'active',
|
'active',
|
||||||
'show_captions',
|
'show_captions',
|
||||||
'show_license',
|
'show_license',
|
||||||
|
@ -285,7 +425,11 @@ class PortfolioController extends Controller
|
||||||
'profile_source'
|
'profile_source'
|
||||||
]));
|
]));
|
||||||
|
|
||||||
Cache::forget('portfolio:recent-feed:' . $request->user()->profile_id);
|
Cache::forget(self::RECENT_FEED_KEY . $pid);
|
||||||
|
|
||||||
|
if($clearFeedCache) {
|
||||||
|
Cache::forget(self::RSS_FEED_KEY . $pid);
|
||||||
|
}
|
||||||
|
|
||||||
return 200;
|
return 200;
|
||||||
}
|
}
|
||||||
|
@ -295,7 +439,7 @@ class PortfolioController extends Controller
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'ids' => 'required|array|max:24'
|
'ids' => 'required|array|max:100'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
@ -308,11 +452,117 @@ class PortfolioController extends Controller
|
||||||
->findOrFail($ids);
|
->findOrFail($ids);
|
||||||
|
|
||||||
$p = Portfolio::whereProfileId($pid)->firstOrFail();
|
$p = Portfolio::whereProfileId($pid)->firstOrFail();
|
||||||
$p->metadata = ['posts' => $ids];
|
$metadata = $p->metadata;
|
||||||
|
$metadata['posts'] = $ids;
|
||||||
|
$p->metadata = $metadata;
|
||||||
$p->save();
|
$p->save();
|
||||||
|
|
||||||
Cache::forget('portfolio:recent-feed:' . $pid);
|
Cache::forget(self::RECENT_FEED_KEY . $pid);
|
||||||
|
Cache::forget(self::RSS_FEED_KEY . $pid);
|
||||||
|
Cache::forget(self::CACHED_FEED_KEY . $pid);
|
||||||
|
|
||||||
return $request->ids;
|
return $request->ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRssFeed(Request $request, $username)
|
||||||
|
{
|
||||||
|
$user = User::whereUsername($username)->first();
|
||||||
|
|
||||||
|
if(!$user) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereUserId($user->id)->where('active', 1)->firstOrFail();
|
||||||
|
|
||||||
|
$metadata = $portfolio->metadata;
|
||||||
|
|
||||||
|
abort_if(!$metadata || !isset($metadata['rss_enabled']), 404);
|
||||||
|
abort_unless($metadata['rss_enabled'], 404);
|
||||||
|
|
||||||
|
$account = AccountService::get($user->profile_id);
|
||||||
|
$portfolioUrl = $portfolio->url();
|
||||||
|
$portfolioLayout = $portfolio->profile_layout;
|
||||||
|
|
||||||
|
if(!isset($metadata['posts']) || !count($metadata['posts'])) {
|
||||||
|
$feed = [];
|
||||||
|
} else {
|
||||||
|
$feed = Cache::remember(
|
||||||
|
self::RSS_FEED_KEY . $user->profile_id,
|
||||||
|
43200,
|
||||||
|
function() use($portfolio, $portfolioUrl, $portfolioLayout) {
|
||||||
|
return collect($portfolio->metadata['posts'])->map(function($post) {
|
||||||
|
return StatusService::get($post);
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->map(function($post, $idx) use($portfolioLayout, $portfolioUrl) {
|
||||||
|
$ts = now()->parse($post['created_at']);
|
||||||
|
$url = $portfolioLayout == 'album' ? $portfolioUrl . '?slide=' . ($idx + 1) : $portfolioUrl . '/' . $post['id'];
|
||||||
|
return [
|
||||||
|
'title' => 'Post by ' . $post['account']['username'] . ' on ' . $ts->format('D, d M Y'),
|
||||||
|
'description' => $post['content_text'],
|
||||||
|
'pubDate' => date('D, d M Y H:i:s ', strtotime($post['created_at'])) . 'GMT',
|
||||||
|
'url' => $url
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->reverse()
|
||||||
|
->take(10)
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('D, d M Y H:i:s ') . 'GMT';
|
||||||
|
|
||||||
|
return response()
|
||||||
|
->view('portfolio.rss_feed', compact('account', 'now', 'feed', 'portfolioUrl'), 200)
|
||||||
|
->header('Content-Type', 'text/xml');
|
||||||
|
return response($feed)->withHeaders(['Content-Type' => 'text/xml']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getApFeed(Request $request, $username)
|
||||||
|
{
|
||||||
|
$user = User::whereUsername($username)->first();
|
||||||
|
|
||||||
|
if(!$user) {
|
||||||
|
return view('portfolio.404');
|
||||||
|
}
|
||||||
|
|
||||||
|
$portfolio = Portfolio::whereUserId($user->id)->where('active', 1)->firstOrFail();
|
||||||
|
$metadata = $portfolio->metadata;
|
||||||
|
$baseUrl = config('app.url');
|
||||||
|
$page = $request->input('page');
|
||||||
|
|
||||||
|
$res = [
|
||||||
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id' => $portfolio->permalink('.json'),
|
||||||
|
'type' => 'OrderedCollection',
|
||||||
|
'totalItems' => isset($metadata['posts']) ? count($metadata['posts']) : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
if($request->has('page')) {
|
||||||
|
$start = $page == 1 ? 0 : ($page * 10 - 10);
|
||||||
|
$res['id'] = $portfolio->permalink('.json?page=' . $page);
|
||||||
|
$res['type'] = 'OrderedCollectionPage';
|
||||||
|
$res['next'] = $portfolio->permalink('.json?page=' . $page + 1);
|
||||||
|
$res['partOf'] = $portfolio->permalink('.json');
|
||||||
|
$res['orderedItems'] = collect($metadata['posts'])->slice($start)->take(10)->map(function($p) {
|
||||||
|
return StatusService::get($p);
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->map(function($p) {
|
||||||
|
return $p['url'];
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if(!$res['orderedItems'] || $res['orderedItems']->count() != 10) {
|
||||||
|
unset($res['next']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$res['first'] = $portfolio->permalink('.json?page=1');
|
||||||
|
}
|
||||||
|
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)
|
||||||
|
->header('Content-Type', 'application/activity+json');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,13 +28,20 @@ class Portfolio extends Model
|
||||||
'metadata' => 'json'
|
'metadata' => 'json'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function url()
|
public function url($suffix = '')
|
||||||
{
|
{
|
||||||
$account = AccountService::get($this->profile_id);
|
$account = AccountService::get($this->profile_id);
|
||||||
if(!$account) {
|
if(!$account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'https://' . config('portfolio.domain') . config('portfolio.path') . '/' . $account['username'];
|
return 'https://' . config('portfolio.domain') . config('portfolio.path') . '/' . $account['username'] . $suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permalink($suffix = '')
|
||||||
|
{
|
||||||
|
$account = AccountService::get($this->profile_id);
|
||||||
|
|
||||||
|
return config('app.url') . '/account/portfolio/' . $account['username'] . $suffix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,11 @@
|
||||||
<div class="col-12 mb-4">
|
<div class="col-12 mb-4">
|
||||||
<p v-if="settings.show_captions && post.content_text">{{ post.content_text }}</p>
|
<p v-if="settings.show_captions && post.content_text">{{ post.content_text }}</p>
|
||||||
<div class="d-md-flex justify-content-between align-items-center">
|
<div class="d-md-flex justify-content-between align-items-center">
|
||||||
<p class="small text-lighter">by <a :href="profileUrl()" class="text-lighter font-weight-bold">@{{profile.username}}</a></p>
|
<p class="small">by <a :href="profileUrl()" class="font-weight-bold link-color">@{{profile.username}}</a></p>
|
||||||
<p v-if="settings.show_license && post.media_attachments[0].license" class="small text-muted">Licensed under {{ post.media_attachments[0].license.title }}</p>
|
<p v-if="settings.show_license && post.media_attachments[0].license" class="small">Licensed under {{ post.media_attachments[0].license.title }}</p>
|
||||||
<p v-if="settings.show_location && post.place" class="small text-muted">{{ post.place.name }}, {{ post.place.country }}</p>
|
<p v-if="settings.show_location && post.place" class="small text-muted">{{ post.place.name }}, {{ post.place.country }}</p>
|
||||||
<p v-if="settings.show_timestamp" class="small text-muted">
|
<p v-if="settings.show_timestamp" class="small">
|
||||||
<a v-if="settings.show_link" :href="post.url" class="text-lighter font-weight-bold" style="z-index: 2">
|
<a v-if="settings.show_link" :href="post.url" class="font-weight-bold link-color" style="z-index: 2">
|
||||||
{{ formatDate(post.created_at) }}
|
{{ formatDate(post.created_at) }}
|
||||||
</a>
|
</a>
|
||||||
<span v-else class="user-select-none">
|
<span v-else class="user-select-none">
|
||||||
|
@ -96,6 +96,15 @@
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.settings = res.data;
|
this.settings = res.data;
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('background_color')) {
|
||||||
|
this.updateCssVariable('--body-bg', res.data.background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('text_color')) {
|
||||||
|
this.updateCssVariable('--text-color', res.data.text_color);
|
||||||
|
this.updateCssVariable('--link-color', res.data.text_color);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -116,6 +125,11 @@
|
||||||
formatDate(ts) {
|
formatDate(ts) {
|
||||||
const dts = new Date(ts);
|
const dts = new Date(ts);
|
||||||
return dts.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric' });
|
return dts.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCssVariable(k, v) {
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,35 @@
|
||||||
<div class="row py-5">
|
<div class="row py-5">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex align-items-center flex-column">
|
<div class="d-flex align-items-center flex-column">
|
||||||
<img :src="profile.avatar" width="60" height="60" class="rounded-circle shadow" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
|
<img
|
||||||
|
v-if="settings.show_avatar"
|
||||||
|
:src="profile.avatar"
|
||||||
|
width="60"
|
||||||
|
height="60"
|
||||||
|
class="rounded-circle shadow"
|
||||||
|
onerror="this.src='/storage/avatars/default.png?v=0';this.onerror=null;">
|
||||||
|
|
||||||
<div class="py-3 text-center" style="max-width: 60%">
|
<div class="py-3 text-center" style="max-width: 60%">
|
||||||
<h1 class="font-weight-bold">{{ profile.username }}</h1>
|
<h1 class="font-weight-bold">{{ profile.username }}</h1>
|
||||||
<p class="font-weight-light mb-0">{{ profile.note_text }}</p>
|
<p v-if="settings.show_bio" class="font-weight-light mb-0 text-break">{{ profile.note_text }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="settings.show_profile_button || (settings.rss_enabled && settings.show_rss_button)" class="pb-3 text-center d-flex flex-column flex-sm-row" style="max-width: 60%;gap: 1rem;">
|
||||||
|
<a
|
||||||
|
v-if="settings.show_profile_button"
|
||||||
|
class="btn btn-outline-primary btn-custom-color"
|
||||||
|
:href="profile.url"
|
||||||
|
target="_blank">
|
||||||
|
View Profile
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="settings.rss_enabled && settings.show_rss_button"
|
||||||
|
class="btn btn-outline-primary btn-custom-color"
|
||||||
|
:href="settings.rss_feed_url"
|
||||||
|
target="_blank">
|
||||||
|
<i class="far fa-rss"></i> RSS
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,7 +50,23 @@
|
||||||
<div v-for="(res, index) in feed" class="col-12 col-md-4 mb-1 p-1">
|
<div v-for="(res, index) in feed" class="col-12 col-md-4 mb-1 p-1">
|
||||||
<div class="square">
|
<div class="square">
|
||||||
<a :href="postUrl(res)">
|
<a :href="postUrl(res)">
|
||||||
<img :src="res.media_attachments[0].url" width="100%" height="300" style="overflow: hidden;object-fit: cover;" class="square-content pr-1">
|
<div class="lazy-img">
|
||||||
|
<blur-hash-canvas
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
:hash="res.media_attachments[0].blurhash"
|
||||||
|
class="square-content pr-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src=""
|
||||||
|
:data-src="res.media_attachments[0].url"
|
||||||
|
width="100%"
|
||||||
|
height="300"
|
||||||
|
style="overflow: hidden;object-fit: cover;z-index: -1;"
|
||||||
|
class="square-content pr-1 img-placeholder"
|
||||||
|
loading="lazy" />
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,14 +74,14 @@
|
||||||
|
|
||||||
<div v-else-if="settings.profile_layout ==='album'" class="col-12 mb-1 p-1">
|
<div v-else-if="settings.profile_layout ==='album'" class="col-12 mb-1 p-1">
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<p class="text-muted font-weight-bold">{{ albumIndex + 1 }} <span class="font-weight-light">/</span> {{ feed.length }}</p>
|
<p class="text-color font-weight-bold">{{ albumIndex + 1 }} <span class="font-weight-light">/</span> {{ feed.length }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span v-if="albumIndex === 0">
|
<span v-if="albumIndex === 0">
|
||||||
<i class="fa fa-arrow-circle-left fa-3x text-dark" />
|
<i class="fa fa-arrow-circle-left fa-3x text-color-lighter" />
|
||||||
</span>
|
</span>
|
||||||
<a v-else @click.prevent="albumPrev()" href="#">
|
<a v-else @click.prevent="albumPrev()" href="#">
|
||||||
<i class="fa fa-arrow-circle-left fa-3x text-muted"/>
|
<i class="fa fa-arrow-circle-left fa-3x text-color"/>
|
||||||
</a>
|
</a>
|
||||||
<transition name="slide-fade">
|
<transition name="slide-fade">
|
||||||
<a :href="postUrl(feed[albumIndex])" class="mx-4" :key="albumIndex">
|
<a :href="postUrl(feed[albumIndex])" class="mx-4" :key="albumIndex">
|
||||||
|
@ -55,10 +95,10 @@
|
||||||
</a>
|
</a>
|
||||||
</transition>
|
</transition>
|
||||||
<span v-if="albumIndex === feed.length - 1">
|
<span v-if="albumIndex === feed.length - 1">
|
||||||
<i class="fa fa-arrow-circle-right fa-3x text-dark" />
|
<i class="fa fa-arrow-circle-right fa-3x text-color-lighter" />
|
||||||
</span>
|
</span>
|
||||||
<a v-else @click.prevent="albumNext()" href="#">
|
<a v-else @click.prevent="albumNext()" href="#">
|
||||||
<i class="fa fa-arrow-circle-right fa-3x text-muted"/>
|
<i class="fa fa-arrow-circle-right fa-3x text-color"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,13 +106,13 @@
|
||||||
<div v-else-if="settings.profile_layout ==='masonry'" class="col-12 p-0 m-0">
|
<div v-else-if="settings.profile_layout ==='masonry'" class="col-12 p-0 m-0">
|
||||||
<div v-for="(res, index) in feed" class="p-1">
|
<div v-for="(res, index) in feed" class="p-1">
|
||||||
<a :href="postUrl(res)" data-fancybox="recent" :data-src="res.media_attachments[0].url" :data-width="res.media_attachments[0].width" :data-height="res.media_attachments[0].height">
|
<a :href="postUrl(res)" data-fancybox="recent" :data-src="res.media_attachments[0].url" :data-width="res.media_attachments[0].width" :data-height="res.media_attachments[0].height">
|
||||||
<img
|
<img
|
||||||
:src="res.media_attachments[0].url"
|
:src="res.media_attachments[0].url"
|
||||||
width="100%"
|
width="100%"
|
||||||
class="user-select-none"
|
class="user-select-none"
|
||||||
style="overflow: hidden;object-fit: contain;"
|
style="overflow: hidden;object-fit: contain;"
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,7 +127,7 @@
|
||||||
<span class="text-gradient-primary">portfolio</span>
|
<span class="text-gradient-primary">portfolio</span>
|
||||||
</span>
|
</span>
|
||||||
<p v-if="user && user.id == profile.id" class="text-center mb-0">
|
<p v-if="user && user.id == profile.id" class="text-center mb-0">
|
||||||
<a :href="settingsUrl" class="text-muted"><i class="far fa-cog fa-lg"></i></a>
|
<a :href="settingsUrl" class="link-color"><i class="far fa-cog fa-lg"></i></a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +149,7 @@
|
||||||
settings: undefined,
|
settings: undefined,
|
||||||
feed: [],
|
feed: [],
|
||||||
albumIndex: 0,
|
albumIndex: 0,
|
||||||
settingsUrl: window._portfolio.path + '/settings'
|
settingsUrl: window._portfolio.path + '/settings',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -135,6 +175,15 @@
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.settings = res.data;
|
this.settings = res.data;
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('background_color')) {
|
||||||
|
this.updateCssVariable('--body-bg', res.data.background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('text_color')) {
|
||||||
|
this.updateCssVariable('--text-color', res.data.text_color);
|
||||||
|
this.updateCssVariable('--link-color', res.data.text_color);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchFeed();
|
this.fetchFeed();
|
||||||
|
@ -145,7 +194,7 @@
|
||||||
async fetchFeed() {
|
async fetchFeed() {
|
||||||
axios.get('/api/portfolio/' + this.profile.id + '/feed')
|
axios.get('/api/portfolio/' + this.profile.id + '/feed')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.feed = res.data.filter(p => p.pf_type === "photo");
|
this.feed = res.data.filter(p => ['photo', 'photo:album'].includes(p.pf_type));
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setAlbumSlide();
|
this.setAlbumSlide();
|
||||||
|
@ -162,6 +211,11 @@
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.bootIntersectors()
|
||||||
|
}, 500);
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
postUrl(res) {
|
postUrl(res) {
|
||||||
|
@ -217,6 +271,38 @@
|
||||||
gutter: 20,
|
gutter: 20,
|
||||||
modal: false,
|
modal: false,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCssVariable(k, v) {
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty(k, v);
|
||||||
|
},
|
||||||
|
|
||||||
|
bootIntersectors() {
|
||||||
|
var lazyImages = [].slice.call(document.querySelectorAll("img.img-placeholder"));
|
||||||
|
|
||||||
|
if ("IntersectionObserver" in window) {
|
||||||
|
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
|
||||||
|
entries.forEach(function(entry) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
let lazyImage = entry.target;
|
||||||
|
lazyImage.src = lazyImage.dataset.src;
|
||||||
|
lazyImage.style.zIndex = 2;
|
||||||
|
lazyImage.classList.remove("img-placeholder");
|
||||||
|
lazyImageObserver.unobserve(lazyImage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyImages.forEach(function(lazyImage) {
|
||||||
|
lazyImageObserver.observe(lazyImage);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
lazyImages.forEach(function(img) {
|
||||||
|
img.src = img.dataset.src;
|
||||||
|
img.style.zIndex = 2;
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,10 @@
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="mt-n2 mb-4">
|
<div class="mt-n2 mb-4">
|
||||||
<p class="text-muted small">Select up to 24 photos from your 100 most recent posts. You can only select public photo posts, videos are not supported at this time.</p>
|
<p class="text-muted small">Select up to 100 photos from your 100 most recent posts. You can only select public photo posts, videos are not supported at this time.</p>
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<p class="font-weight-bold mb-0">Selected {{ selectedRecentPosts.length }}/24</p>
|
<p class="font-weight-bold mb-0">Selected {{ selectedRecentPosts.length }}/100</p>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-link font-weight-bold mr-3 text-decoration-none"
|
class="btn btn-link font-weight-bold mr-3 text-decoration-none"
|
||||||
|
@ -90,11 +90,13 @@
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<img
|
<img
|
||||||
:key="post.id"
|
:key="post.id"
|
||||||
:src="post.media_attachments[0].url"
|
:src="getPreviewUrl(post)"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="300"
|
height="300"
|
||||||
style="overflow: hidden;object-fit: cover;"
|
style="overflow: hidden;object-fit: cover;"
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
|
loading="lazy"
|
||||||
|
onerror="this.src='/storage/no-preview.png';this.onerror=null;"
|
||||||
class="square-content pr-1">
|
class="square-content pr-1">
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
|
@ -112,45 +114,128 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="tabIndex === 'Customize'" class="col-12 col-md-8 mt-3 py-2" key="2">
|
<div v-else-if="tabIndex === 'Customize'" class="col-12 mt-3 py-2" key="2">
|
||||||
<div v-for="setting in customizeSettings" class="card bg-dark mb-5">
|
<div class="row">
|
||||||
<div class="card-header">{{ setting.title }}</div>
|
<div class="col-12 col-md-6">
|
||||||
<div class="list-group bg-dark">
|
<div v-for="setting in customizeSettings" class="card bg-dark mb-5">
|
||||||
<div v-for="item in setting.items" class="list-group-item">
|
<div class="card-header">{{ setting.title }}</div>
|
||||||
<div class="d-flex justify-content-between align-items-center py-2">
|
<div class="list-group bg-dark">
|
||||||
<div class="setting-label">
|
<div v-for="item in setting.items" class="list-group-item">
|
||||||
<p class="mb-0">{{ item.label }}</p>
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
<p v-if="item.description" class="small text-muted mb-0">{{ item.description }}</p>
|
<div class="setting-label">
|
||||||
</div>
|
<p class="mb-0">{{ item.label }}</p>
|
||||||
|
<p v-if="item.description" class="small text-muted mb-0">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-switch mt-n1">
|
<div class="setting-switch mt-n1">
|
||||||
<b-form-checkbox
|
<b-form-checkbox
|
||||||
v-model="settings[item.model]"
|
v-model="settings[item.model]"
|
||||||
name="check-button"
|
name="check-button"
|
||||||
size="lg"
|
size="lg"
|
||||||
switch
|
switch
|
||||||
:disabled="item.requiredWithTrue && !settings[item.requiredWithTrue]" />
|
:disabled="item.requiredWithTrue && !settings[item.requiredWithTrue]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card bg-dark mb-5">
|
</div>
|
||||||
<div class="card-header">Portfolio</div>
|
|
||||||
<div class="list-group bg-dark">
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="d-flex justify-content-between align-items-center py-2">
|
|
||||||
<div class="setting-label">
|
|
||||||
<p class="mb-0">Layout</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="col-12 col-md-6">
|
||||||
<b-form-select v-model="settings.profile_layout" :options="profileLayoutOptions" />
|
<div class="card bg-dark mb-5">
|
||||||
</div>
|
<div class="card-header">Portfolio</div>
|
||||||
</div>
|
<div class="list-group bg-dark">
|
||||||
</div>
|
<div class="list-group-item">
|
||||||
</div>
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
</div>
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Layout</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b-form-select v-model="settings.profile_layout" :options="profileLayoutOptions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="settings.profile_source === 'custom'" class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Order</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b-form-select
|
||||||
|
v-model="settings.feed_order"
|
||||||
|
:options="profileLayoutFeedOrder" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Color Scheme</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b-form-select
|
||||||
|
v-model="settings.color_scheme"
|
||||||
|
:options="profileLayoutColorSchemeOptions"
|
||||||
|
:disabled="settings.color_scheme === 'custom'"
|
||||||
|
@change="updateColorScheme" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Background Color</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-col sm="2">
|
||||||
|
<b-form-input
|
||||||
|
v-model="settings.background_color"
|
||||||
|
debounce="1000"
|
||||||
|
type="color"
|
||||||
|
@change="updateBackgroundColor" />
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
v-if="!['#000000', null].includes(settings.background_color)"
|
||||||
|
variant="link"
|
||||||
|
@click="resetBackgroundColor">
|
||||||
|
Reset
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center py-2">
|
||||||
|
<div class="setting-label">
|
||||||
|
<p class="mb-0">Text Color</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-col sm="2">
|
||||||
|
<b-form-input
|
||||||
|
v-model="settings.text_color"
|
||||||
|
debounce="1000"
|
||||||
|
type="color"
|
||||||
|
@change="updateTextColor" />
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
v-if="!['#d4d4d8', null].includes(settings.text_color)"
|
||||||
|
variant="link"
|
||||||
|
@click="resetTextColor">
|
||||||
|
Reset
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="tabIndex === 'Share'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="0">
|
<div v-else-if="tabIndex === 'Share'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="0">
|
||||||
|
@ -185,6 +270,7 @@
|
||||||
isSavingCurated: false,
|
isSavingCurated: false,
|
||||||
canSaveCurated: false,
|
canSaveCurated: false,
|
||||||
customizeSettings: [],
|
customizeSettings: [],
|
||||||
|
skipWatch: false,
|
||||||
profileSourceOptions: [
|
profileSourceOptions: [
|
||||||
{ value: null, text: 'Please select an option', disabled: true },
|
{ value: null, text: 'Please select an option', disabled: true },
|
||||||
{ value: 'recent', text: 'Most recent posts' },
|
{ value: 'recent', text: 'Most recent posts' },
|
||||||
|
@ -194,6 +280,16 @@
|
||||||
{ value: 'grid', text: 'Grid' },
|
{ value: 'grid', text: 'Grid' },
|
||||||
{ value: 'masonry', text: 'Masonry' },
|
{ value: 'masonry', text: 'Masonry' },
|
||||||
{ value: 'album', text: 'Album' },
|
{ value: 'album', text: 'Album' },
|
||||||
|
],
|
||||||
|
profileLayoutColorSchemeOptions: [
|
||||||
|
{ value: null, text: 'Please select an option', disabled: true },
|
||||||
|
{ value: 'light', text: 'Light mode' },
|
||||||
|
{ value: 'dark', text: 'Dark mode' },
|
||||||
|
{ value: 'custom', text: 'Custom color scheme', disabled: true },
|
||||||
|
],
|
||||||
|
profileLayoutFeedOrder: [
|
||||||
|
{ value: 'oldest', text: 'Oldest first' },
|
||||||
|
{ value: 'recent', text: 'Recent first' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -217,7 +313,7 @@
|
||||||
deep: true,
|
deep: true,
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler: function(o, n) {
|
handler: function(o, n) {
|
||||||
if(this.loading) {
|
if(this.loading || this.skipWatch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!n.show_timestamp) {
|
if(!n.show_timestamp) {
|
||||||
|
@ -260,6 +356,20 @@
|
||||||
if(res.data.metadata && res.data.metadata.posts) {
|
if(res.data.metadata && res.data.metadata.posts) {
|
||||||
this.selectedRecentPosts = res.data.metadata.posts;
|
this.selectedRecentPosts = res.data.metadata.posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(res.data.color_scheme != 'dark') {
|
||||||
|
if(res.data.color_scheme === 'light') {
|
||||||
|
this.updateBackgroundColor('#ffffff');
|
||||||
|
} else {
|
||||||
|
if(res.data.hasOwnProperty('background_color')) {
|
||||||
|
this.updateBackgroundColor(res.data.background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(res.data.hasOwnProperty('text_color')) {
|
||||||
|
this.updateTextColor(res.data.text_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.initCustomizeSettings();
|
this.initCustomizeSettings();
|
||||||
|
@ -325,16 +435,22 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSettings() {
|
updateSettings(silent = false) {
|
||||||
|
if(this.skipWatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
axios.post(this.apiPath('/api/portfolio/self/update-settings.json'), this.settings)
|
axios.post(this.apiPath('/api/portfolio/self/update-settings.json'), this.settings)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.updateTabs();
|
this.updateTabs();
|
||||||
this.$bvToast.toast(`Your settings have been successfully updated!`, {
|
if(!silent) {
|
||||||
variant: 'dark',
|
this.$bvToast.toast(`Your settings have been successfully updated!`, {
|
||||||
title: 'Settings Updated',
|
variant: 'dark',
|
||||||
autoHideDelay: 2000,
|
title: 'Settings Updated',
|
||||||
appendToast: false
|
autoHideDelay: 2000,
|
||||||
})
|
appendToast: false
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -354,7 +470,7 @@
|
||||||
|
|
||||||
toggleRecentPost(id) {
|
toggleRecentPost(id) {
|
||||||
if(this.selectedRecentPosts.indexOf(id) == -1) {
|
if(this.selectedRecentPosts.indexOf(id) == -1) {
|
||||||
if(this.selectedRecentPosts.length === 24) {
|
if(this.selectedRecentPosts.length === 100) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.selectedRecentPosts.push(id);
|
this.selectedRecentPosts.push(id);
|
||||||
|
@ -449,10 +565,105 @@
|
||||||
{
|
{
|
||||||
label: "Show Bio",
|
label: "Show Bio",
|
||||||
model: "show_bio"
|
model: "show_bio"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
label: "Show View Profile Button",
|
||||||
|
model: "show_profile_button"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enable RSS Feed",
|
||||||
|
description: "Enable your RSS feed with the 10 most recent portfolio items",
|
||||||
|
model: "rss_enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Show RSS Feed Button",
|
||||||
|
model: "show_rss_button",
|
||||||
|
requiredWithTrue: "rss_enabled"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBackgroundColor(e) {
|
||||||
|
this.skipWatch = true;
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty('--body-bg', e);
|
||||||
|
|
||||||
|
if(e !== '#000000' && e !== '#ffffff') {
|
||||||
|
this.settings.color_scheme = 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTextColor(e) {
|
||||||
|
this.skipWatch = true;
|
||||||
|
let rs = document.querySelector(':root');
|
||||||
|
rs.style.setProperty('--text-color', e);
|
||||||
|
|
||||||
|
if(e !== '#d4d4d8') {
|
||||||
|
this.settings.color_scheme = 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetBackgroundColor() {
|
||||||
|
this.skipWatch = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateBackgroundColor('#000000');
|
||||||
|
this.settings.color_scheme = 'dark';
|
||||||
|
this.settings.background_color = '#000000';
|
||||||
|
this.updateSettings(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
resetTextColor() {
|
||||||
|
this.skipWatch = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateTextColor('#d4d4d8');
|
||||||
|
this.settings.color_scheme = 'dark';
|
||||||
|
this.settings.text_color = '#d4d4d8';
|
||||||
|
this.updateSettings(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.skipWatch = false;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateColorScheme(e) {
|
||||||
|
if(e === 'light') {
|
||||||
|
this.updateBackgroundColor('#ffffff');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e === 'dark') {
|
||||||
|
this.updateBackgroundColor('#000000');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPreviewUrl(post) {
|
||||||
|
let media = post.media_attachments[0];
|
||||||
|
if(!media) { return '/storage/no-preview.png'; }
|
||||||
|
|
||||||
|
if(media.preview_url && !media.preview_url.endsWith('/no-preview.png')) {
|
||||||
|
return media.preview_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return media.url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
resources/assets/js/portfolio.js
vendored
5
resources/assets/js/portfolio.js
vendored
|
@ -1,7 +1,10 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
window.Vue = Vue;
|
window.Vue = Vue;
|
||||||
import BootstrapVue from 'bootstrap-vue'
|
import BootstrapVue from 'bootstrap-vue';
|
||||||
|
import VueBlurHash from 'vue-blurhash';
|
||||||
|
import 'vue-blurhash/dist/vue-blurhash.css'
|
||||||
Vue.use(BootstrapVue);
|
Vue.use(BootstrapVue);
|
||||||
|
Vue.use(VueBlurHash);
|
||||||
|
|
||||||
Vue.component(
|
Vue.component(
|
||||||
'portfolio-post',
|
'portfolio-post',
|
||||||
|
|
50
resources/assets/sass/portfolio.scss
vendored
50
resources/assets/sass/portfolio.scss
vendored
|
@ -1,23 +1,67 @@
|
||||||
@import "lib/inter";
|
@import "lib/inter";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--body-bg: #000000;
|
||||||
|
--text-color: #d4d4d8;
|
||||||
|
--link-color: #3B82F6;
|
||||||
|
};
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #000000;
|
background: var(--body-bg);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
color: #d4d4d8;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: #3B82F6 !important;
|
color: #3B82F6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-color {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-color-lighter {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-custom-color {
|
||||||
|
border-color: var(--link-color);
|
||||||
|
color: var(--link-color);
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 7px 30px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--link-color) !important;
|
||||||
|
color: var(--link-color) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-color {
|
||||||
|
color: var(--link-color);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: var(--link-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lead,
|
.lead,
|
||||||
.font-weight-light {
|
.font-weight-light {
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #3B82F6;
|
color: var(--link-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
resources/views/portfolio/rss_feed.blade.php
Normal file
20
resources/views/portfolio/rss_feed.blade.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- RSS generated by pixelfed v{{config('pixelfed.version')}} on {{$now}} -->
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>{{ $account['username'] }}'s Portfolio</title>
|
||||||
|
<link>{{ $portfolioUrl }}</link>
|
||||||
|
<description>The pixelfed portfolio of {{ $account['username'] }} with the {{ count($feed) }} most recent posts</description>
|
||||||
|
<pubDate>{{ $now }}</pubDate>
|
||||||
|
<language>en-us</language>
|
||||||
|
@foreach($feed as $p)
|
||||||
|
<item>
|
||||||
|
<title>{{$p['title']}}</title>
|
||||||
|
<description>{{$p['description']}}</description>
|
||||||
|
<guid>{{$p['url']}}</guid>
|
||||||
|
<link>{{$p['url']}}</link>
|
||||||
|
<pubDate>{{$p['pubDate']}}</pubDate>
|
||||||
|
</item>
|
||||||
|
@endforeach
|
||||||
|
</channel>
|
||||||
|
</rss>
|
|
@ -407,6 +407,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
||||||
Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests');
|
Route::get('follow-requests', 'AccountController@followRequests')->name('follow-requests');
|
||||||
Route::post('follow-requests', 'AccountController@followRequestHandle');
|
Route::post('follow-requests', 'AccountController@followRequestHandle');
|
||||||
Route::get('follow-requests.json', 'AccountController@followRequestsJson');
|
Route::get('follow-requests.json', 'AccountController@followRequestsJson');
|
||||||
|
Route::get('portfolio/{username}.json', 'PortfolioController@getApFeed');
|
||||||
|
Route::get('portfolio/{username}.rss', 'PortfolioController@getRssFeed');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::group(['prefix' => 'settings'], function () {
|
Route::group(['prefix' => 'settings'], function () {
|
||||||
|
|
Loading…
Reference in a new issue