mirror of https://github.com/pixelfed/pixelfed
commit
cbb98b0462
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Media;
|
||||
use App\Jobs\VideoPipeline\VideoThumbnail as Pipeline;
|
||||
|
||||
class VideoThumbnail extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'video:thumbnail';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate missing video thumbnails';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = 10;
|
||||
$videos = Media::whereMime('video/mp4')
|
||||
->whereNull('thumbnail_path')
|
||||
->take($limit)
|
||||
->get();
|
||||
foreach($videos as $video) {
|
||||
Pipeline::dispatchNow($video);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Cache;
|
||||
|
||||
class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'mentions',
|
||||
'media_attachments',
|
||||
'tags',
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
|
||||
'reblog' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'created_at' => $status->created_at->format('c'),
|
||||
'emojis' => [],
|
||||
'reblogs_count' => $status->shares()->count(),
|
||||
'favourites_count' => $status->likes()->count(),
|
||||
'reblogged' => null,
|
||||
'favourited' => null,
|
||||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary,
|
||||
'visibility' => $status->visibility,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
],
|
||||
'language' => null,
|
||||
'pinned' => null,
|
||||
|
||||
'pf_type' => $status->type ?? $status->setType(),
|
||||
'reply_count' => (int) $status->reply_count,
|
||||
'comments_disabled' => $status->comments_disabled ? true : false,
|
||||
'thread' => false,
|
||||
'replies' => [],
|
||||
'parent' => $status->parent() ? $this->transform($status->parent()) : [],
|
||||
];
|
||||
}
|
||||
|
||||
public function includeAccount(Status $status)
|
||||
{
|
||||
$account = $status->profile;
|
||||
|
||||
return $this->item($account, new AccountTransformer());
|
||||
}
|
||||
|
||||
public function includeMentions(Status $status)
|
||||
{
|
||||
$mentions = $status->mentions;
|
||||
|
||||
return $this->collection($mentions, new MentionTransformer());
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addMinutes(3), function() use($status) {
|
||||
if(in_array($status->type, ['photo', 'video'])) {
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$tags = $status->hashtags;
|
||||
|
||||
return $this->collection($tags, new HashtagTransformer());
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* This package comes with multi tenancy out of the box. Here you can
|
||||
* configure the different apps that can use the webSockets server.
|
||||
*
|
||||
* Optionally you can disable client events so clients cannot send
|
||||
* messages to each other via the webSockets.
|
||||
*/
|
||||
'apps' => [
|
||||
[
|
||||
'id' => env('PUSHER_APP_ID'),
|
||||
'name' => env('APP_NAME'),
|
||||
'key' => env('PUSHER_APP_KEY'),
|
||||
'secret' => env('PUSHER_APP_SECRET'),
|
||||
'enable_client_messages' => env('WSS_CM', false),
|
||||
'enable_statistics' => env('WSS_STATS', false),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* This class is responsible for finding the apps. The default provider
|
||||
* will use the apps defined in this config file.
|
||||
*
|
||||
* You can create a custom provider by implementing the
|
||||
* `AppProvider` interface.
|
||||
*/
|
||||
'app_provider' => BeyondCode\LaravelWebSockets\Apps\ConfigAppProvider::class,
|
||||
|
||||
/*
|
||||
* This array contains the hosts of which you want to allow incoming requests.
|
||||
* Leave this empty if you want to accept requests from all hosts.
|
||||
*/
|
||||
'allowed_origins' => [
|
||||
//
|
||||
],
|
||||
|
||||
/*
|
||||
* The maximum request size in kilobytes that is allowed for an incoming WebSocket request.
|
||||
*/
|
||||
'max_request_size_in_kb' => 250,
|
||||
|
||||
/*
|
||||
* This path will be used to register the necessary routes for the package.
|
||||
*/
|
||||
'path' => 'laravel-websockets',
|
||||
|
||||
/*
|
||||
* Dashboard Routes Middleware
|
||||
*
|
||||
* These middleware will be assigned to every dashboard route, giving you
|
||||
* the chance to add your own middleware to this list or change any of
|
||||
* the existing middleware. Or, you can simply stick with this list.
|
||||
*/
|
||||
'middleware' => [
|
||||
'web',
|
||||
Authorize::class,
|
||||
],
|
||||
|
||||
'statistics' => [
|
||||
/*
|
||||
* This model will be used to store the statistics of the WebSocketsServer.
|
||||
* The only requirement is that the model should extend
|
||||
* `WebSocketsStatisticsEntry` provided by this package.
|
||||
*/
|
||||
'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,
|
||||
|
||||
/*
|
||||
* Here you can specify the interval in seconds at which statistics should be logged.
|
||||
*/
|
||||
'interval_in_seconds' => 60,
|
||||
|
||||
/*
|
||||
* When the clean-command is executed, all recorded statistics older than
|
||||
* the number of days specified here will be deleted.
|
||||
*/
|
||||
'delete_statistics_older_than_days' => 60,
|
||||
|
||||
/*
|
||||
* Use an DNS resolver to make the requests to the statistics logger
|
||||
* default is to resolve everything to 127.0.0.1.
|
||||
*/
|
||||
'perform_dns_lookup' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
* Define the optional SSL context for your WebSocket connections.
|
||||
* You can see all available options at: http://php.net/manual/en/context.ssl.php
|
||||
*/
|
||||
'ssl' => [
|
||||
/*
|
||||
* Path to local certificate file on filesystem. It must be a PEM encoded file which
|
||||
* contains your certificate and private key. It can optionally contain the
|
||||
* certificate chain of issuers. The private key also may be contained
|
||||
* in a separate file specified by local_pk.
|
||||
*/
|
||||
'local_cert' => null,
|
||||
|
||||
/*
|
||||
* Path to local private key file on filesystem in case of separate files for
|
||||
* certificate (local_cert) and private key.
|
||||
*/
|
||||
'local_pk' => null,
|
||||
|
||||
/*
|
||||
* Passphrase for your local_cert file.
|
||||
*/
|
||||
'passphrase' => null,
|
||||
],
|
||||
|
||||
/*
|
||||
* Channel Manager
|
||||
* This class handles how channel persistence is handled.
|
||||
* By default, persistence is stored in an array by the running webserver.
|
||||
* The only requirement is that the class should implement
|
||||
* `ChannelManager` interface provided by this package.
|
||||
*/
|
||||
'channel_manager' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class,
|
||||
];
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,15 +1,16 @@
|
||||
{
|
||||
"/js/activity.js": "/js/activity.js?id=7915246c3bc2b7e9770e",
|
||||
"/js/activity.js": "/js/activity.js?id=d148322a0d90e0afe4df",
|
||||
"/js/app.js": "/js/app.js?id=1f05f00eec0e86f49dd4",
|
||||
"/css/app.css": "/css/app.css?id=3a974ff74b6b5905a73c",
|
||||
"/css/appdark.css": "/css/appdark.css?id=107806a000e2ca675a3c",
|
||||
"/css/landing.css": "/css/landing.css?id=d3610108213e88dc080c",
|
||||
"/js/components.js": "/js/components.js?id=25d082643150ee79150c",
|
||||
"/js/compose.js": "/js/compose.js?id=041385233a3b1ed64d28",
|
||||
"/js/components.js": "/js/components.js?id=ddc135bc319514161701",
|
||||
"/js/compose.js": "/js/compose.js?id=f69f248dc1cfcb8ae092",
|
||||
"/js/developers.js": "/js/developers.js?id=1359f11c7349301903f8",
|
||||
"/js/discover.js": "/js/discover.js?id=75fb12b06ee23fa05186",
|
||||
"/js/profile.js": "/js/profile.js?id=742fc029b8b7591f04bf",
|
||||
"/js/search.js": "/js/search.js?id=0d3d080dc05f4f49b204",
|
||||
"/js/status.js": "/js/status.js?id=d30b9926fe4a4b2feee5",
|
||||
"/js/timeline.js": "/js/timeline.js?id=bc70f81d24b488ef564d"
|
||||
"/js/discover.js": "/js/discover.js?id=0625385218493e556ccc",
|
||||
"/js/loops.js": "/js/loops.js?id=3e3276ee44e1d5a27d0b",
|
||||
"/js/profile.js": "/js/profile.js?id=257d02b221142438d173",
|
||||
"/js/search.js": "/js/search.js?id=27e8be8bfef6be586d25",
|
||||
"/js/status.js": "/js/status.js?id=fb2f77026b548814adc3",
|
||||
"/js/timeline.js": "/js/timeline.js?id=b507c500a328f8160177"
|
||||
}
|
||||
|
@ -1,10 +1,4 @@
|
||||
$(document).ready(function() {
|
||||
$('.pagination').hide();
|
||||
let elem = document.querySelector('.notification-page .list-group');
|
||||
let infScroll = new InfiniteScroll( elem, {
|
||||
path: '.pagination__next',
|
||||
append: '.notification-page .list-group',
|
||||
status: '.page-load-status',
|
||||
history: true,
|
||||
});
|
||||
});
|
||||
Vue.component(
|
||||
'activity-component',
|
||||
require('./components/Activity.vue').default
|
||||
);
|
@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- <div class="bg-white py-4">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div></div>
|
||||
<a href="/account/activity" class="cursor-pointer font-weight-bold text-primary">Notifications</a>
|
||||
<a href="/account/direct" class="cursor-pointer font-weight-bold text-dark">Direct Messages</a>
|
||||
<a href="/account/following" class="cursor-pointer font-weight-bold text-dark">Following</a>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="container">
|
||||
<div class="row my-5">
|
||||
<div class="col-12 col-md-8 offset-md-2">
|
||||
<div v-if="notifications.length > 0" class="media mb-3 align-items-center px-3 border-bottom pb-3" v-for="(n, index) in notifications">
|
||||
<img class="mr-2 rounded-circle" style="border:1px solid #ccc" :src="n.account.avatar" alt="" width="32px" height="32px">
|
||||
<div class="media-body font-weight-light">
|
||||
<div v-if="n.type == 'favourite'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> liked your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'comment'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> commented on your <a class="font-weight-bold" v-bind:href="n.status.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'mention'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> <a class="font-weight-bold" v-bind:href="mentionUrl(n.status)">mentioned</a> you.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'follow'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> followed you.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="n.type == 'share'">
|
||||
<p class="my-0">
|
||||
<a :href="n.account.url" class="font-weight-bold text-dark word-break" data-placement="bottom" data-toggle="tooltip" :title="n.account.username">{{truncate(n.account.username)}}</a> shared your <a class="font-weight-bold" v-bind:href="n.status.reblog.url">post</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="align-items-center">
|
||||
<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="n.status && n.status && n.status.media_attachments && n.status.media_attachments.length">
|
||||
<a :href="n.status.url">
|
||||
<img :src="n.status.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
|
||||
<a :href="n.status.parent.url">
|
||||
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div v-else-if="n.status && n.status.parent && n.status.parent.media_attachments && n.status.parent.media_attachments.length">
|
||||
<a :href="n.status.parent.url">
|
||||
<img :src="n.status.parent.media_attachments[0].preview_url" width="32px" height="32px">
|
||||
</a>
|
||||
</div> -->
|
||||
<div v-else-if="n.type == 'follow' && n.relationship.following == false">
|
||||
<a href="#" class="btn btn-primary py-0 font-weight-bold" @click.prevent="followProfile(n);">
|
||||
Follow
|
||||
</a>
|
||||
</div>
|
||||
<!-- <div v-else-if="n.status && n.status.parent && !n.status.parent.media_attachments && n.type == 'like' && n.relationship.following == false">
|
||||
<a href="#" class="btn btn-primary py-0 font-weight-bold">
|
||||
Follow
|
||||
</a>
|
||||
</div> -->
|
||||
<div v-else>
|
||||
<a class="btn btn-outline-primary py-0 font-weight-bold" :href="viewContext(n)">View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifications.length">
|
||||
<infinite-loading @infinite="infiniteNotifications">
|
||||
<div slot="no-results" class="font-weight-bold"></div>
|
||||
<div slot="no-more" class="font-weight-bold"></div>
|
||||
</infinite-loading>
|
||||
</div>
|
||||
<div v-if="notifications.length == 0" class="text-lighter text-center py-3">
|
||||
<p class="mb-0"><i class="fas fa-inbox fa-3x"></i></p>
|
||||
<p class="mb-0 small font-weight-bold">0 Notifications!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/javascript">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
notifications: {},
|
||||
notificationCursor: 2,
|
||||
notificationMaxId: 0,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchNotifications();
|
||||
},
|
||||
|
||||
updated() {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchNotifications() {
|
||||
axios.get('/api/v1/notifications', {
|
||||
params: {
|
||||
pg: true
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
let data = res.data.filter(n => {
|
||||
if(n.type == 'share' && !status) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
let ids = res.data.map(n => n.id);
|
||||
this.notificationMaxId = Math.max(...ids);
|
||||
this.notifications = data;
|
||||
$('.notification-card .loader').addClass('d-none');
|
||||
$('.notification-card .contents').removeClass('d-none');
|
||||
});
|
||||
},
|
||||
|
||||
infiniteNotifications($state) {
|
||||
if(this.notificationCursor > 10) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
axios.get('/api/v1/notifications', {
|
||||
params: {
|
||||
page: this.notificationCursor,
|
||||
pg: true
|
||||
}
|
||||
}).then(res => {
|
||||
if(res.data.length > 0) {
|
||||
let data = res.data.filter(n => {
|
||||
if(n.type == 'share' && !status) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.notifications.push(...data);
|
||||
this.notificationCursor++;
|
||||
$state.loaded();
|
||||
} else {
|
||||
$state.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
truncate(text) {
|
||||
if(text.length <= 15) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0,15) + '...'
|
||||
},
|
||||
|
||||
timeAgo(ts) {
|
||||
let date = Date.parse(ts);
|
||||
let seconds = Math.floor((new Date() - date) / 1000);
|
||||
let interval = Math.floor(seconds / 31536000);
|
||||
if (interval >= 1) {
|
||||
return interval + "y";
|
||||
}
|
||||
interval = Math.floor(seconds / 604800);
|
||||
if (interval >= 1) {
|
||||
return interval + "w";
|
||||
}
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval >= 1) {
|
||||
return interval + "d";
|
||||
}
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval >= 1) {
|
||||
return interval + "h";
|
||||
}
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval >= 1) {
|
||||
return interval + "m";
|
||||
}
|
||||
return Math.floor(seconds) + "s";
|
||||
},
|
||||
|
||||
mentionUrl(status) {
|
||||
let username = status.account.username;
|
||||
let id = status.id;
|
||||
return '/p/' + username + '/' + id;
|
||||
},
|
||||
|
||||
followProfile(n) {
|
||||
let self = this;
|
||||
let id = n.account.id;
|
||||
axios.post('/i/follow', {
|
||||
item: id
|
||||
}).then(res => {
|
||||
self.notifications.map(notification => {
|
||||
if(notification.account.id === id) {
|
||||
notification.relationship.following = true;
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
if(err.response.data.message) {
|
||||
swal('Error', err.response.data.message, 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
viewContext(n) {
|
||||
switch(n.type) {
|
||||
case 'follow':
|
||||
return n.account.url;
|
||||
break;
|
||||
case 'mention':
|
||||
return n.status.url;
|
||||
break;
|
||||
case 'like':
|
||||
case 'favourite':
|
||||
return n.status.url;
|
||||
break;
|
||||
}
|
||||
return '/';
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<p class="text-center">
|
||||
<!-- <a :class="[tab == 'popular'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('popular')">Popular</a> -->
|
||||
<a :class="[tab == 'new'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('new')">New</a>
|
||||
<!-- <a :class="[tab == 'random'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('random')">Random</a> -->
|
||||
<a :class="[tab == 'about'? 'btn font-weight-bold py-0 btn-success' : 'btn font-weight-bold py-0 btn-outline-success']" href="#" @click.prevent="setTab('about')">About</a>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="tab != 'about'" class="row loops-container">
|
||||
<div class="col-12 col-md-4 mb-3" v-for="(loop, index) in loops">
|
||||
<div class="card border border-success">
|
||||
<div class="embed-responsive embed-responsive-1by1">
|
||||
<video class="embed-responsive-item" :src="videoSrc(loop)" preload="auto" loop @click="toggleVideo(loop, $event)"></video>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="username font-weight-bolder"><a :href="loop.account.url">{{loop.account.acct}}</a> , <a :href="loop.url">{{timestamp(loop)}}</a></p>
|
||||
<p class="small text-muted" v-html="loop.content"></p>
|
||||
<div class="small text-muted d-flex justify-content-between mb-0">
|
||||
<span>{{loop.favourites_count}} Likes</span>
|
||||
<span>{{loop.reblogs_count}} Shares</span>
|
||||
<span>0 Loops</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="lead text-center mb-0">Loops are an exciting new way to explore short videos on Pixelfed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="text/css">
|
||||
.loops-container .card {
|
||||
box-shadow: none;
|
||||
}
|
||||
.loops-container .card .card-img-top{
|
||||
border-radius: 0;
|
||||
}
|
||||
.loops-container a {
|
||||
color: #343a40;
|
||||
}
|
||||
a.hashtag,
|
||||
.loops-container .card-body a:hover {
|
||||
color: #28a745 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
Object.defineProperty(HTMLMediaElement.prototype, 'playing', {
|
||||
get: function(){
|
||||
return !!(this.currentTime > 0 && !this.paused && !this.ended && this.readyState > 2);
|
||||
}
|
||||
})
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
'version': 1,
|
||||
'loops': [],
|
||||
'tab': 'new'
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
axios.get('/api/v2/loops')
|
||||
.then(res => {
|
||||
this.loops = res.data;
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
videoSrc(loop) {
|
||||
return loop.media_attachments[0].url;
|
||||
},
|
||||
setTab(tab) {
|
||||
this.tab = tab;
|
||||
},
|
||||
toggleVideo(loop, $event) {
|
||||
let el = $event.target;
|
||||
$('video').each(function() {
|
||||
if(el.src != $(this)[0].src) {
|
||||
$(this)[0].pause();
|
||||
}
|
||||
});
|
||||
if(!el.playing) {
|
||||
el.play();
|
||||
this.incrementLoop(loop);
|
||||
} else {
|
||||
el.pause();
|
||||
}
|
||||
},
|
||||
incrementLoop(loop) {
|
||||
axios.post('/api/v2/loops/watch', {
|
||||
id: loop.id
|
||||
}).then(res => {
|
||||
console.log(res.data);
|
||||
});
|
||||
},
|
||||
timestamp(loop) {
|
||||
let ts = new Date(loop.created_at);
|
||||
return ts.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,4 @@
|
||||
Vue.component(
|
||||
'direct-component',
|
||||
require('./components/Direct.vue').default
|
||||
);
|
@ -0,0 +1,4 @@
|
||||
Vue.component(
|
||||
'loops-component',
|
||||
require('./components/LoopComponent.vue').default
|
||||
);
|
@ -0,0 +1,93 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 py-5 d-flex justify-content-between align-items-center">
|
||||
<p class="h1 mb-0"><i class="far fa-circle"></i> Create Circle</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-10 offset-md-1">
|
||||
<div class="card">
|
||||
<div class="card-body px-5">
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="post">
|
||||
@csrf
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" placeholder="Circle Name" name="name" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" placeholder="Optional description visible only to you" rows="3" name="description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Visibility</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" name="scope">
|
||||
<option value="public">Public</option>
|
||||
<option value="unlisted">Unlisted</option>
|
||||
<option value="private">Followers Only</option>
|
||||
<option value="exclusive">Circle Only</option>
|
||||
</select>
|
||||
<p class="help-text font-weight-bold text-muted small">Who can view posts from this circle</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-2 font-weight-bold text-muted">BCC Mode</div>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="bcc">
|
||||
<label class="form-check-label"></label>
|
||||
</div>
|
||||
<p class="help-text mb-0 small text-muted">Send posts without mentioning other circle recipients.</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label font-weight-bold text-muted">Members</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10 offset-sm-2">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" name="active" id="activeSwitch">
|
||||
<label class="custom-control-label font-weight-bold text-muted" for="activeSwitch">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-right mb-0">
|
||||
<button type="submit" class="btn btn-primary btn-sm py-1 font-weight-bold">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@endpush
|
@ -0,0 +1,17 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div>
|
||||
<direct-component></direct-component>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ mix('js/direct.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '#content'
|
||||
});
|
||||
</script>
|
||||
@endpush
|
@ -0,0 +1,39 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-success" style="height:1.2px;"></div>
|
||||
<div class="profile-header">
|
||||
<div class="container pt-5">
|
||||
<div class="profile-details text-center">
|
||||
<div class="username-bar text-dark">
|
||||
<p class="display-4 font-weight-bold mb-0"><span class="text-success">Loops</span> <sup class="lead">BETA</sup></p>
|
||||
<p class="lead font-weight-lighter">Short looping videos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loop-page container mt-5">
|
||||
<section>
|
||||
<loops-component></loops-component>
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style type="text/css">
|
||||
@media (min-width: 1200px) {
|
||||
.loop-page.container {
|
||||
max-width: 1035px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{ mix('js/loops.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
new Vue({el: '#content'});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
Loading…
Reference in New Issue