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
|
||||
{
|
||||
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)
|
||||
{
|
||||
return view('portfolio.index');
|
||||
|
@ -60,11 +64,11 @@ class PortfolioController extends Controller
|
|||
$user = AccountService::get($post['account']['id']);
|
||||
$portfolio = Portfolio::whereProfileId($user['id'])->first();
|
||||
|
||||
if($user['locked'] || $portfolio->active != true) {
|
||||
if(!$portfolio || $user['locked'] || $portfolio->active != true) {
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -117,7 +121,7 @@ class PortfolioController extends Controller
|
|||
$this->validate($request, [
|
||||
'profile_source' => 'required|in:recent,custom',
|
||||
'layout' => 'required|in:grid,masonry',
|
||||
'layout_container' => 'required|in:fixed,fluid'
|
||||
'layout_container' => 'required|in:fixed,fluid',
|
||||
]);
|
||||
|
||||
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
|
||||
|
@ -140,6 +144,7 @@ class PortfolioController extends Controller
|
|||
$portfolio->show_bio = $request->input('show_bio') === 'on';
|
||||
$portfolio->profile_layout = $request->input('layout');
|
||||
$portfolio->profile_container = $request->input('layout_container');
|
||||
$portfolio->metadata = $metadata;
|
||||
$portfolio->save();
|
||||
|
||||
return redirect('/' . $request->user()->username);
|
||||
|
@ -171,16 +176,24 @@ class PortfolioController extends Controller
|
|||
return response()->json([], 400);
|
||||
}
|
||||
|
||||
$feed = Cache::remember(self::CACHED_FEED_KEY . $portfolio->profile_id, 86400, function() use($portfolio) {
|
||||
return collect($portfolio->metadata['posts'])->map(function($p) {
|
||||
return StatusService::get($p);
|
||||
})
|
||||
->filter(function($p) {
|
||||
return $p && isset($p['account']);
|
||||
})->values();
|
||||
});
|
||||
});
|
||||
|
||||
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) {
|
||||
$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')
|
||||
->whereProfileId($id)
|
||||
->whereNotNull('status_id')
|
||||
|
@ -215,6 +228,14 @@ class PortfolioController extends Controller
|
|||
}
|
||||
|
||||
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 [
|
||||
'url' => $p->url(),
|
||||
'pid' => (string) $p->profile_id,
|
||||
|
@ -228,6 +249,13 @@ class PortfolioController extends Controller
|
|||
'show_bio' => (bool) $p->show_bio,
|
||||
'profile_layout' => $p->profile_layout,
|
||||
'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
|
||||
];
|
||||
})->first();
|
||||
|
@ -248,8 +276,13 @@ class PortfolioController extends Controller
|
|||
if(!$p) {
|
||||
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(),
|
||||
'show_captions' => (bool) $p->show_captions,
|
||||
'show_license' => (bool) $p->show_license,
|
||||
|
@ -259,8 +292,27 @@ class PortfolioController extends Controller
|
|||
'show_avatar' => (bool) $p->show_avatar,
|
||||
'show_bio' => (bool) $p->show_bio,
|
||||
'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)
|
||||
|
@ -268,11 +320,99 @@ class PortfolioController extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$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)
|
||||
->update($request->only([
|
||||
$res = Portfolio::whereUserId($request->user()->id)->firstOrFail();
|
||||
$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',
|
||||
'show_captions',
|
||||
'show_license',
|
||||
|
@ -285,7 +425,11 @@ class PortfolioController extends Controller
|
|||
'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;
|
||||
}
|
||||
|
@ -295,7 +439,7 @@ class PortfolioController extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'ids' => 'required|array|max:24'
|
||||
'ids' => 'required|array|max:100'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
@ -308,11 +452,117 @@ class PortfolioController extends Controller
|
|||
->findOrFail($ids);
|
||||
|
||||
$p = Portfolio::whereProfileId($pid)->firstOrFail();
|
||||
$p->metadata = ['posts' => $ids];
|
||||
$metadata = $p->metadata;
|
||||
$metadata['posts'] = $ids;
|
||||
$p->metadata = $metadata;
|
||||
$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;
|
||||
}
|
||||
|
||||
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'
|
||||
];
|
||||
|
||||
public function url()
|
||||
public function url($suffix = '')
|
||||
{
|
||||
$account = AccountService::get($this->profile_id);
|
||||
if(!$account) {
|
||||
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">
|
||||
<p v-if="settings.show_captions && post.content_text">{{ post.content_text }}</p>
|
||||
<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 v-if="settings.show_license && post.media_attachments[0].license" class="small text-muted">Licensed under {{ post.media_attachments[0].license.title }}</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">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_timestamp" class="small text-muted">
|
||||
<a v-if="settings.show_link" :href="post.url" class="text-lighter font-weight-bold" style="z-index: 2">
|
||||
<p v-if="settings.show_timestamp" class="small">
|
||||
<a v-if="settings.show_link" :href="post.url" class="font-weight-bold link-color" style="z-index: 2">
|
||||
{{ formatDate(post.created_at) }}
|
||||
</a>
|
||||
<span v-else class="user-select-none">
|
||||
|
@ -96,6 +96,15 @@
|
|||
})
|
||||
.then(res => {
|
||||
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(() => {
|
||||
setTimeout(() => {
|
||||
|
@ -116,6 +125,11 @@
|
|||
formatDate(ts) {
|
||||
const dts = new Date(ts);
|
||||
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="col-12">
|
||||
<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%">
|
||||
<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>
|
||||
|
@ -26,7 +50,23 @@
|
|||
<div v-for="(res, index) in feed" class="col-12 col-md-4 mb-1 p-1">
|
||||
<div class="square">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,14 +74,14 @@
|
|||
|
||||
<div v-else-if="settings.profile_layout ==='album'" class="col-12 mb-1 p-1">
|
||||
<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 class="d-flex justify-content-between align-items-center">
|
||||
<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>
|
||||
<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>
|
||||
<transition name="slide-fade">
|
||||
<a :href="postUrl(feed[albumIndex])" class="mx-4" :key="albumIndex">
|
||||
|
@ -55,10 +95,10 @@
|
|||
</a>
|
||||
</transition>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -87,7 +127,7 @@
|
|||
<span class="text-gradient-primary">portfolio</span>
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -109,7 +149,7 @@
|
|||
settings: undefined,
|
||||
feed: [],
|
||||
albumIndex: 0,
|
||||
settingsUrl: window._portfolio.path + '/settings'
|
||||
settingsUrl: window._portfolio.path + '/settings',
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -135,6 +175,15 @@
|
|||
})
|
||||
.then(res => {
|
||||
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(() => {
|
||||
this.fetchFeed();
|
||||
|
@ -145,7 +194,7 @@
|
|||
async fetchFeed() {
|
||||
axios.get('/api/portfolio/' + this.profile.id + '/feed')
|
||||
.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(() => {
|
||||
this.setAlbumSlide();
|
||||
|
@ -162,6 +211,11 @@
|
|||
}, 500);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
this.bootIntersectors()
|
||||
}, 500);
|
||||
})
|
||||
},
|
||||
|
||||
postUrl(res) {
|
||||
|
@ -217,6 +271,38 @@
|
|||
gutter: 20,
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
<button
|
||||
class="btn btn-link font-weight-bold mr-3 text-decoration-none"
|
||||
|
@ -90,11 +90,13 @@
|
|||
<transition name="fade">
|
||||
<img
|
||||
:key="post.id"
|
||||
:src="post.media_attachments[0].url"
|
||||
:src="getPreviewUrl(post)"
|
||||
width="100%"
|
||||
height="300"
|
||||
style="overflow: hidden;object-fit: cover;"
|
||||
:draggable="false"
|
||||
loading="lazy"
|
||||
onerror="this.src='/storage/no-preview.png';this.onerror=null;"
|
||||
class="square-content pr-1">
|
||||
</transition>
|
||||
|
||||
|
@ -112,7 +114,9 @@
|
|||
</template>
|
||||
</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 class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div v-for="setting in customizeSettings" class="card bg-dark mb-5">
|
||||
<div class="card-header">{{ setting.title }}</div>
|
||||
<div class="list-group bg-dark">
|
||||
|
@ -135,6 +139,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card bg-dark mb-5">
|
||||
<div class="card-header">Portfolio</div>
|
||||
<div class="list-group bg-dark">
|
||||
|
@ -149,6 +156,84 @@
|
|||
</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>
|
||||
|
@ -185,6 +270,7 @@
|
|||
isSavingCurated: false,
|
||||
canSaveCurated: false,
|
||||
customizeSettings: [],
|
||||
skipWatch: false,
|
||||
profileSourceOptions: [
|
||||
{ value: null, text: 'Please select an option', disabled: true },
|
||||
{ value: 'recent', text: 'Most recent posts' },
|
||||
|
@ -194,6 +280,16 @@
|
|||
{ value: 'grid', text: 'Grid' },
|
||||
{ value: 'masonry', text: 'Masonry' },
|
||||
{ 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,
|
||||
immediate: true,
|
||||
handler: function(o, n) {
|
||||
if(this.loading) {
|
||||
if(this.loading || this.skipWatch) {
|
||||
return;
|
||||
}
|
||||
if(!n.show_timestamp) {
|
||||
|
@ -260,6 +356,20 @@
|
|||
if(res.data.metadata && 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(() => {
|
||||
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)
|
||||
.then(res => {
|
||||
this.updateTabs();
|
||||
if(!silent) {
|
||||
this.$bvToast.toast(`Your settings have been successfully updated!`, {
|
||||
variant: 'dark',
|
||||
title: 'Settings Updated',
|
||||
autoHideDelay: 2000,
|
||||
appendToast: false
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -354,7 +470,7 @@
|
|||
|
||||
toggleRecentPost(id) {
|
||||
if(this.selectedRecentPosts.indexOf(id) == -1) {
|
||||
if(this.selectedRecentPosts.length === 24) {
|
||||
if(this.selectedRecentPosts.length === 100) {
|
||||
return;
|
||||
}
|
||||
this.selectedRecentPosts.push(id);
|
||||
|
@ -449,10 +565,105 @@
|
|||
{
|
||||
label: "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';
|
||||
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(VueBlurHash);
|
||||
|
||||
Vue.component(
|
||||
'portfolio-post',
|
||||
|
|
50
resources/assets/sass/portfolio.scss
vendored
50
resources/assets/sass/portfolio.scss
vendored
|
@ -1,23 +1,67 @@
|
|||
@import "lib/inter";
|
||||
|
||||
:root {
|
||||
--body-bg: #000000;
|
||||
--text-color: #d4d4d8;
|
||||
--link-color: #3B82F6;
|
||||
};
|
||||
|
||||
body {
|
||||
background: #000000;
|
||||
background: var(--body-bg);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 400 !important;
|
||||
color: #d4d4d8;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
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,
|
||||
.font-weight-light {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3B82F6;
|
||||
color: var(--link-color);
|
||||
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::post('follow-requests', 'AccountController@followRequestHandle');
|
||||
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 () {
|
||||
|
|
Loading…
Reference in a new issue