Merge pull request #2738 from pixelfed/staging

Staging
This commit is contained in:
daniel 2021-04-29 23:55:10 -06:00 committed by GitHub
commit 1b7ef61a4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 871 additions and 421 deletions

View file

@ -67,6 +67,11 @@
- Updated AdminInstanceController, invalidate banned domain cache when updated. ([35393edf](https://github.com/pixelfed/pixelfed/commit/35393edf))
- Updated AP Helpers, use instance filtering. ([66b4f8c7](https://github.com/pixelfed/pixelfed/commit/66b4f8c7))
- Updated ApiV1Controller, add missing instance api attributes. ([64b86546](https://github.com/pixelfed/pixelfed/commit/64b86546))
- Updated story garbage collection, handle non active stories and new ephemeral story media directory. ([c43f8bcc](https://github.com/pixelfed/pixelfed/commit/c43f8bcc))
- Updated Stories, add crop and duration settings to composer. ([c8edca69](https://github.com/pixelfed/pixelfed/commit/c8edca69))
- Updated instance endpoint, add custom description. ([668e936e](https://github.com/pixelfed/pixelfed/commit/668e936e))
- Updated StoryCompose component, improve full screen preview. ([39a76103](https://github.com/pixelfed/pixelfed/commit/39a76103))
- Updated Helpers, fix broken tests. ([22dddaa0](https://github.com/pixelfed/pixelfed/commit/22dddaa0))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10)

View file

@ -3,103 +3,127 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\{
DB,
Storage
};
use App\{
Story,
StoryView
};
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use App\Story;
use App\StoryView;
class StoryGC extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'story:gc';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'story:gc';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear expired Stories';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear expired Stories';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->directoryScan();
$this->deleteViews();
$this->deleteStories();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->directoryScan();
$this->deleteViews();
$this->deleteStories();
}
protected function directoryScan()
{
$day = now()->day;
protected function directoryScan()
{
$hour = now()->hour;
if($day != 3) {
return;
}
if($hour !== 1) {
return;
}
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
$t1 = Storage::directories('public/_esm.t1');
$t2 = Storage::directories('public/_esm.t2');
$t1 = Storage::directories('public/_esm.t1');
$t2 = Storage::directories('public/_esm.t2');
$dirs = array_merge($t1, $t2);
$dirs = array_merge($t1, $t2);
foreach($dirs as $dir) {
$hash = last(explode('/', $dir));
if($hash != $monthHash) {
$this->info('Found directory to delete: ' . $dir);
$this->deleteDirectory($dir);
}
}
}
foreach($dirs as $dir) {
$hash = last(explode('/', $dir));
if($hash != $monthHash) {
$this->info('Found directory to delete: ' . $dir);
$this->deleteDirectory($dir);
}
}
protected function deleteDirectory($path)
{
Storage::deleteDirectory($path);
}
$mh = hash('sha256', date('Y').'-.-'.date('m'));
$monthHash = date('Y').date('m').substr($mh, 0, 6).substr($mh, 58, 6);
$dirs = Storage::directories('public/_esm.t3');
protected function deleteViews()
{
StoryView::where('created_at', '<', now()->subMinutes(1441))->delete();
}
foreach($dirs as $dir) {
$hash = last(explode('/', $dir));
if($hash != $monthHash) {
$this->info('Found directory to delete: ' . $dir);
$this->deleteDirectory($dir);
}
}
}
protected function deleteStories()
{
$stories = Story::where('created_at', '<', now()->subMinutes(1441))->take(50)->get();
protected function deleteDirectory($path)
{
Storage::deleteDirectory($path);
}
if($stories->count() == 0) {
exit;
}
protected function deleteViews()
{
StoryView::where('created_at', '<', now()->subMinutes(1441))->delete();
}
foreach($stories as $story) {
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
DB::transaction(function() use($story) {
StoryView::whereStoryId($story->id)->delete();
$story->delete();
});
}
}
protected function deleteStories()
{
$stories = Story::where('created_at', '>', now()->subMinutes(30))
->whereNull('active')
->get();
foreach($stories as $story) {
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
DB::transaction(function() use($story) {
StoryView::whereStoryId($story->id)->delete();
$story->delete();
});
}
$stories = Story::where('created_at', '<', now()
->subMinutes(1441))
->get();
if($stories->count() == 0) {
exit;
}
foreach($stories as $story) {
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
}
DB::transaction(function() use($story) {
StoryView::whereStoryId($story->id)->delete();
$story->delete();
});
}
}
}

View file

@ -48,6 +48,11 @@ class Handler extends ExceptionHandler
*/
public function render($request, Throwable $exception)
{
if ($request->wantsJson())
return response()->json(
['error' => $exception->getMessage()],
method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500
);
return parent::render($request, $exception);
}
}

View file

@ -960,7 +960,7 @@ class ApiV1Controller extends Controller
$res = [
'approval_required' => false,
'contact_account' => null,
'description' => 'Pixelfed - Photo sharing for everyone',
'description' => config('instance.description'),
'email' => config('instance.email'),
'invites_enabled' => false,
'rules' => [],

View file

@ -12,7 +12,7 @@ use App\Services\StoryService;
use Cache, Storage;
use Image as Intervention;
use App\Services\FollowerService;
use App\Services\MediaPathService;
class StoryController extends Controller
{
@ -37,7 +37,7 @@ class StoryController extends Controller
}
$photo = $request->file('file');
$path = $this->storePhoto($photo);
$path = $this->storePhoto($photo, $user);
$story = new Story();
$story->duration = 3;
@ -47,21 +47,18 @@ class StoryController extends Controller
$story->path = $path;
$story->local = true;
$story->size = $photo->getSize();
$story->expires_at = now()->addHours(24);
$story->save();
return [
'code' => 200,
'msg' => 'Successfully added',
'media_id' => (string) $story->id,
'media_url' => url(Storage::url($story->path))
];
}
protected function storePhoto($photo)
protected function storePhoto($photo, $user)
{
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
$sid = (string) Str::uuid();
$rid = Str::random(9).'.'.Str::random(9);
$mimes = explode(',', config('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [
'image/jpeg',
@ -72,9 +69,9 @@ class StoryController extends Controller
return;
}
$storagePath = "public/_esm.t2/{$monthHash}/{$sid}/{$rid}";
$storagePath = MediaPathService::story($user->profile);
$path = $photo->store($storagePath);
if(in_array($photo->getMimeType(), ['image/jpeg','image/png',])) {
if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) {
$fpath = storage_path('app/' . $path);
$img = Intervention::make($fpath);
$img->orientate();
@ -84,6 +81,68 @@ class StoryController extends Controller
return $path;
}
public function cropPhoto(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'media_id' => 'required|integer|min:1',
'width' => 'required',
'height' => 'required',
'x' => 'required',
'y' => 'required'
]);
$user = $request->user();
$id = $request->input('media_id');
$width = round($request->input('width'));
$height = round($request->input('height'));
$x = round($request->input('x'));
$y = round($request->input('y'));
$story = Story::whereProfileId($user->profile_id)->findOrFail($id);
$path = storage_path('app/' . $story->path);
if(!is_file($path)) {
abort(400, 'Invalid or missing media.');
}
$img = Intervention::make($path);
$img->crop($width, $height, $x, $y);
$img->save($path, config('pixelfed.image_quality'));
return [
'code' => 200,
'msg' => 'Successfully cropped',
];
}
public function publishStory(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'media_id' => 'required',
'duration' => 'required|integer|min:3|max:10'
]);
$id = $request->input('media_id');
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
$story->active = true;
$story->duration = $request->input('duration', 10);
$story->expires_at = now()->addHours(24);
$story->save();
return [
'code' => 200,
'msg' => 'Successfully published',
];
}
public function apiV1Delete(Request $request, $id)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
@ -91,7 +150,7 @@ class StoryController extends Controller
$user = $request->user();
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
->findOrFail($id);
if(Storage::exists($story->path) == true) {
Storage::delete($story->path);
@ -114,6 +173,7 @@ class StoryController extends Controller
if(config('database.default') == 'pgsql') {
$db = Story::with('profile')
->whereActive(true)
->whereIn('profile_id', $following)
->where('expires_at', '>', now())
->distinct('profile_id')
@ -121,8 +181,9 @@ class StoryController extends Controller
->get();
} else {
$db = Story::with('profile')
->whereActive(true)
->whereIn('profile_id', $following)
->where('expires_at', '>', now())
->where('created_at', '>', now()->subDay())
->orderByDesc('expires_at')
->groupBy('profile_id')
->take(9)
@ -158,6 +219,7 @@ class StoryController extends Controller
}
$stories = Story::whereProfileId($profile->id)
->whereActive(true)
->orderBy('expires_at', 'desc')
->where('expires_at', '>', now())
->when(!$publicOnly, function($query, $publicOnly) {
@ -187,6 +249,7 @@ class StoryController extends Controller
$authed = $request->user()->profile;
$story = Story::with('profile')
->whereActive(true)
->where('expires_at', '>', now())
->findOrFail($id);
@ -198,11 +261,11 @@ class StoryController extends Controller
}
abort_if(!$publicOnly, 403);
$res = [
'id' => (string) $story->id,
'type' => Str::endsWith($story->path, '.mp4') ? 'video' :'photo',
'length' => 3,
'length' => 10,
'src' => url(Storage::url($story->path)),
'preview' => null,
'link' => null,
@ -227,6 +290,7 @@ class StoryController extends Controller
}
$stories = Story::whereProfileId($profile->id)
->whereActive(true)
->orderBy('expires_at')
->where('expires_at', '>', now())
->when(!$publicOnly, function($query, $publicOnly) {
@ -237,7 +301,7 @@ class StoryController extends Controller
return [
'id' => $s->id,
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
'length' => 3,
'length' => 10,
'src' => url(Storage::url($s->path)),
'preview' => null,
'link' => null,
@ -272,19 +336,21 @@ class StoryController extends Controller
'id' => 'required|integer|min:1|exists:stories',
]);
$id = $request->input('id');
$authed = $request->user()->profile;
$story = Story::with('profile')
->where('expires_at', '>', now())
->orderByDesc('expires_at')
->findOrFail($id);
$profile = $story->profile;
if($story->profile_id == $authed->id) {
$publicOnly = true;
} else {
$publicOnly = (bool) $profile->followedBy($authed);
return [];
}
$publicOnly = (bool) $profile->followedBy($authed);
abort_if(!$publicOnly, 403);
StoryView::firstOrCreate([
@ -292,6 +358,9 @@ class StoryController extends Controller
'profile_id' => $authed->id
]);
$story->view_count = $story->view_count + 1;
$story->save();
return ['code' => 200];
}
@ -300,6 +369,7 @@ class StoryController extends Controller
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
$res = (bool) Story::whereProfileId($id)
->whereActive(true)
->where('expires_at', '>', now())
->count();
@ -312,6 +382,7 @@ class StoryController extends Controller
$profile = $request->user()->profile;
$stories = Story::whereProfileId($profile->id)
->whereActive(true)
->orderBy('expires_at')
->where('expires_at', '>', now())
->get()
@ -346,7 +417,7 @@ class StoryController extends Controller
public function compose(Request $request)
{
abort_if(!config('instance.stories.enabled') || !$request->user(), 404);
return view('stories.compose');
}

View file

@ -12,116 +12,120 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\{
ImportJob,
ImportData,
Media,
Profile,
Status,
ImportJob,
ImportData,
Media,
Profile,
Status,
};
class ImportInstagram implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $import;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(ImportJob $import)
{
$this->import = $import;
}
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if(config('pixelfed.import.instagram.enabled') != true) {
return;
}
protected $import;
$job = ImportJob::findOrFail($this->import->id);
$profile = Profile::findOrFail($job->profile_id);
$user = $profile->user;
$json = $job->mediaJson();
$collection = array_reverse($json['photos']);
$files = $job->files;
$monthHash = hash('sha1', date('Y').date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);
$fs = new Filesystem;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
foreach($collection as $import)
{
$caption = $import['caption'];
try {
$min = Carbon::create(2010, 10, 6, 0, 0, 0);
$taken_at = Carbon::parse($import['taken_at']);
if(!$min->lt($taken_at)) {
$taken_at = Carbon::now();
}
} catch (Exception $e) {
}
$filename = last( explode('/', $import['path']) );
$importData = ImportData::whereJobId($job->id)
->whereOriginalName($filename)
->first();
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(ImportJob $import)
{
$this->import = $import;
}
if(empty($importData) || is_file(storage_path("app/$importData->path")) == false) {
continue;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if(config('pixelfed.import.instagram.enabled') != true) {
return;
}
DB::transaction(function() use(
$fs, $job, $profile, $caption, $taken_at, $filename,
$monthHash, $userHash, $importData
) {
$status = new Status();
$status->profile_id = $profile->id;
$status->caption = strip_tags($caption);
$status->is_nsfw = false;
$status->type = 'photo';
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->created_at = $taken_at;
$status->save();
$job = ImportJob::findOrFail($this->import->id);
$profile = Profile::findOrFail($job->profile_id);
$user = $profile->user;
$json = $job->mediaJson();
$collection = array_reverse($json['photos']);
$files = $job->files;
$monthHash = hash('sha1', date('Y').date('m'));
$userHash = hash('sha1', $user->id . (string) $user->created_at);
$fs = new Filesystem;
foreach($collection as $import)
{
$caption = $import['caption'];
try {
$min = Carbon::create(2010, 10, 6, 0, 0, 0);
$taken_at = Carbon::parse($import['taken_at']);
if(!$min->lt($taken_at)) {
$taken_at = Carbon::now();
}
} catch (Exception $e) {
}
$filename = last( explode('/', $import['path']) );
$importData = ImportData::whereJobId($job->id)
->whereOriginalName($filename)
->first();
if(empty($importData) || is_file(storage_path("app/$importData->path")) == false) {
continue;
}
DB::transaction(function() use(
$fs, $job, $profile, $caption, $taken_at, $filename,
$monthHash, $userHash, $importData
) {
$status = new Status();
$status->profile_id = $profile->id;
$status->caption = strip_tags($caption);
$status->is_nsfw = false;
$status->type = 'photo';
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->created_at = $taken_at;
$status->save();
$path = storage_path("app/$importData->path");
$storagePath = "public/m/{$monthHash}/{$userHash}";
$newPath = "app/$storagePath/$filename";
$fs->move($path,storage_path($newPath));
$path = $newPath;
$hash = \hash_file('sha256', storage_path($path));
$media = new Media();
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$media->user_id = $profile->user->id;
$media->media_path = "$storagePath/$filename";
$media->original_sha256 = $hash;
$media->size = $fs->size(storage_path($path));
$media->mime = $fs->mimeType(storage_path($path));
$media->filter_class = null;
$media->filter_name = null;
$media->order = 1;
$media->save();
ImageOptimize::dispatch($media);
});
}
$path = storage_path("app/$importData->path");
$storagePath = "public/m/{$monthHash}/{$userHash}";
$dir = "app/$storagePath";
if(!is_dir(storage_path($dir))) {
mkdir(storage_path($dir), 0755, true);
}
$newPath = "$dir/$filename";
$fs->move($path,storage_path($newPath));
$path = $newPath;
$hash = \hash_file('sha256', storage_path($path));
$media = new Media();
$media->status_id = $status->id;
$media->profile_id = $profile->id;
$media->user_id = $profile->user->id;
$media->media_path = "$storagePath/$filename";
$media->original_sha256 = $hash;
$media->size = $fs->size(storage_path($path));
$media->mime = $fs->mimeType(storage_path($path));
$media->filter_class = null;
$media->filter_name = null;
$media->order = 1;
$media->save();
ImageOptimize::dispatch($media);
});
}
$job->completed_at = Carbon::now();
$job->save();
}
$job->completed_at = Carbon::now();
$job->save();
}
}

View file

@ -176,12 +176,14 @@ class Helpers {
}
}
$bannedInstances = InstanceService::getBannedDomains();
if(in_array($host, $bannedInstances)) {
return false;
if(app()->environment() === 'production') {
$bannedInstances = InstanceService::getBannedDomains();
if(in_array($host, $bannedInstances)) {
return false;
}
}
if(in_array($host, $localhosts)) {
return false;
}

193
composer.lock generated
View file

@ -130,16 +130,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.178.6",
"version": "3.179.1",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "0aa83b522d5ffa794c02e7411af87a0e241a3082"
"reference": "f4f2c01b53f71379a1ed8ccccf4305949a69ccbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0aa83b522d5ffa794c02e7411af87a0e241a3082",
"reference": "0aa83b522d5ffa794c02e7411af87a0e241a3082",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f4f2c01b53f71379a1ed8ccccf4305949a69ccbe",
"reference": "f4f2c01b53f71379a1ed8ccccf4305949a69ccbe",
"shasum": ""
},
"require": {
@ -214,9 +214,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.178.6"
"source": "https://github.com/aws/aws-sdk-php/tree/3.179.1"
},
"time": "2021-04-19T18:13:17+00:00"
"time": "2021-04-29T18:17:25+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -656,40 +656,39 @@
},
{
"name": "doctrine/cache",
"version": "1.10.2",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "13e3381b25847283a91948d04640543941309727"
"reference": "a9c1b59eba5a08ca2770a76eddb88922f504e8e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727",
"reference": "13e3381b25847283a91948d04640543941309727",
"url": "https://api.github.com/repos/doctrine/cache/zipball/a9c1b59eba5a08ca2770a76eddb88922f504e8e0",
"reference": "a9c1b59eba5a08ca2770a76eddb88922f504e8e0",
"shasum": ""
},
"require": {
"php": "~7.1 || ^8.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
"doctrine/common": ">2.2,<2.4",
"psr/cache": ">=3"
},
"require-dev": {
"alcaeus/mongo-php-adapter": "^1.1",
"doctrine/coding-standard": "^6.0",
"cache/integration-tests": "dev-master",
"doctrine/coding-standard": "^8.0",
"mongodb/mongodb": "^1.1",
"phpunit/phpunit": "^7.0",
"predis/predis": "~1.0"
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"predis/predis": "~1.0",
"psr/cache": "^1.0 || ^2.0",
"symfony/cache": "^4.4 || ^5.2"
},
"suggest": {
"alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
@ -736,7 +735,7 @@
],
"support": {
"issues": "https://github.com/doctrine/cache/issues",
"source": "https://github.com/doctrine/cache/tree/1.10.x"
"source": "https://github.com/doctrine/cache/tree/1.11.0"
},
"funding": [
{
@ -752,7 +751,7 @@
"type": "tidelift"
}
],
"time": "2020-07-07T18:54:01+00:00"
"time": "2021-04-13T14:46:17+00:00"
},
{
"name": "doctrine/dbal",
@ -1517,16 +1516,16 @@
},
{
"name": "fruitcake/laravel-cors",
"version": "v2.0.3",
"version": "v2.0.4",
"source": {
"type": "git",
"url": "https://github.com/fruitcake/laravel-cors.git",
"reference": "01de0fe5f71c70d1930ee9a80385f9cc28e0f63a"
"reference": "a8ccedc7ca95189ead0e407c43b530dc17791d6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruitcake/laravel-cors/zipball/01de0fe5f71c70d1930ee9a80385f9cc28e0f63a",
"reference": "01de0fe5f71c70d1930ee9a80385f9cc28e0f63a",
"url": "https://api.github.com/repos/fruitcake/laravel-cors/zipball/a8ccedc7ca95189ead0e407c43b530dc17791d6a",
"reference": "a8ccedc7ca95189ead0e407c43b530dc17791d6a",
"shasum": ""
},
"require": {
@ -1539,8 +1538,8 @@
},
"require-dev": {
"laravel/framework": "^6|^7|^8",
"orchestra/testbench-dusk": "^4|^5|^6",
"phpunit/phpunit": "^6|^7|^8",
"orchestra/testbench-dusk": "^4|^5|^6|^7",
"phpunit/phpunit": "^6|^7|^8|^9",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
@ -1582,7 +1581,7 @@
],
"support": {
"issues": "https://github.com/fruitcake/laravel-cors/issues",
"source": "https://github.com/fruitcake/laravel-cors/tree/v2.0.3"
"source": "https://github.com/fruitcake/laravel-cors/tree/v2.0.4"
},
"funding": [
{
@ -1590,7 +1589,7 @@
"type": "github"
}
],
"time": "2020-10-22T13:57:20+00:00"
"time": "2021-04-26T11:24:25+00:00"
},
{
"name": "geerlingguy/ping",
@ -1823,16 +1822,16 @@
},
{
"name": "guzzlehttp/psr7",
"version": "1.8.1",
"version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "35ea11d335fd638b5882ff1725228b3d35496ab1"
"reference": "dc960a912984efb74d0a90222870c72c87f10c91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1",
"reference": "35ea11d335fd638b5882ff1725228b3d35496ab1",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
"reference": "dc960a912984efb74d0a90222870c72c87f10c91",
"shasum": ""
},
"require": {
@ -1892,9 +1891,9 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/1.8.1"
"source": "https://github.com/guzzle/psr7/tree/1.8.2"
},
"time": "2021-03-21T16:25:00+00:00"
"time": "2021-04-26T09:17:50+00:00"
},
{
"name": "intervention/image",
@ -2107,16 +2106,16 @@
},
{
"name": "laravel/framework",
"version": "v8.38.0",
"version": "v8.40.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "26a73532c54d2c090692bf2e3e64e449669053ba"
"reference": "a654897ad7f97aea9d7ef292803939798c4a02a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/26a73532c54d2c090692bf2e3e64e449669053ba",
"reference": "26a73532c54d2c090692bf2e3e64e449669053ba",
"url": "https://api.github.com/repos/laravel/framework/zipball/a654897ad7f97aea9d7ef292803939798c4a02a4",
"reference": "a654897ad7f97aea9d7ef292803939798c4a02a4",
"shasum": ""
},
"require": {
@ -2271,7 +2270,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-04-20T13:50:21+00:00"
"time": "2021-04-28T14:38:56+00:00"
},
{
"name": "laravel/helpers",
@ -2331,16 +2330,16 @@
},
{
"name": "laravel/horizon",
"version": "v5.7.5",
"version": "v5.7.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
"reference": "9bf0ef4873d9e52f58a9cd1de69dcdd98a5c4fe8"
"reference": "24ffd819df749ef11a4eb20e14150b671d4f5717"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/horizon/zipball/9bf0ef4873d9e52f58a9cd1de69dcdd98a5c4fe8",
"reference": "9bf0ef4873d9e52f58a9cd1de69dcdd98a5c4fe8",
"url": "https://api.github.com/repos/laravel/horizon/zipball/24ffd819df749ef11a4eb20e14150b671d4f5717",
"reference": "24ffd819df749ef11a4eb20e14150b671d4f5717",
"shasum": ""
},
"require": {
@ -2402,9 +2401,9 @@
],
"support": {
"issues": "https://github.com/laravel/horizon/issues",
"source": "https://github.com/laravel/horizon/tree/v5.7.5"
"source": "https://github.com/laravel/horizon/tree/v5.7.6"
},
"time": "2021-04-06T14:17:52+00:00"
"time": "2021-04-27T18:00:46+00:00"
},
{
"name": "laravel/passport",
@ -3913,16 +3912,16 @@
},
{
"name": "pbmedia/laravel-ffmpeg",
"version": "7.5.10",
"version": "7.5.11",
"source": {
"type": "git",
"url": "https://github.com/protonemedia/laravel-ffmpeg.git",
"reference": "d3c3b77e5de08d4038ebcb9e9d405d51ec8ce2f9"
"reference": "95b75f41edce12f513df12928b10b8f6949ffe56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/d3c3b77e5de08d4038ebcb9e9d405d51ec8ce2f9",
"reference": "d3c3b77e5de08d4038ebcb9e9d405d51ec8ce2f9",
"url": "https://api.github.com/repos/protonemedia/laravel-ffmpeg/zipball/95b75f41edce12f513df12928b10b8f6949ffe56",
"reference": "95b75f41edce12f513df12928b10b8f6949ffe56",
"shasum": ""
},
"require": {
@ -3934,7 +3933,7 @@
"illuminate/support": "^6.0|^7.0|^8.0",
"league/flysystem": "^1.0.34",
"php": "^7.3|^8.0",
"php-ffmpeg/php-ffmpeg": "^0.17.0|^0.18.0"
"php-ffmpeg/php-ffmpeg": "^0.18.0"
},
"require-dev": {
"league/flysystem-memory": "^1.0",
@ -3986,7 +3985,7 @@
],
"support": {
"issues": "https://github.com/protonemedia/laravel-ffmpeg/issues",
"source": "https://github.com/protonemedia/laravel-ffmpeg/tree/7.5.10"
"source": "https://github.com/protonemedia/laravel-ffmpeg/tree/7.5.11"
},
"funding": [
{
@ -3994,7 +3993,7 @@
"type": "github"
}
],
"time": "2021-03-31T14:18:52+00:00"
"time": "2021-04-25T20:47:01+00:00"
},
{
"name": "php-ffmpeg/php-ffmpeg",
@ -5315,16 +5314,16 @@
},
{
"name": "spatie/image-optimizer",
"version": "1.3.2",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/image-optimizer.git",
"reference": "6aa170eb292758553d332efee5e0c3977341080c"
"reference": "c22202fdd57856ed18a79cfab522653291a6e96a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/image-optimizer/zipball/6aa170eb292758553d332efee5e0c3977341080c",
"reference": "6aa170eb292758553d332efee5e0c3977341080c",
"url": "https://api.github.com/repos/spatie/image-optimizer/zipball/c22202fdd57856ed18a79cfab522653291a6e96a",
"reference": "c22202fdd57856ed18a79cfab522653291a6e96a",
"shasum": ""
},
"require": {
@ -5363,9 +5362,9 @@
],
"support": {
"issues": "https://github.com/spatie/image-optimizer/issues",
"source": "https://github.com/spatie/image-optimizer/tree/1.3.2"
"source": "https://github.com/spatie/image-optimizer/tree/1.4.0"
},
"time": "2020-11-28T12:37:58+00:00"
"time": "2021-04-22T06:17:27+00:00"
},
{
"name": "spatie/laravel-backup",
@ -8287,16 +8286,16 @@
"packages-dev": [
{
"name": "brianium/paratest",
"version": "v6.2.0",
"version": "v6.3.0",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "9a94366983ce32c7724fc92e3b544327d4adb9be"
"reference": "268d5b2b4237c0abf76c4aa9633ad8580be01e1e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/9a94366983ce32c7724fc92e3b544327d4adb9be",
"reference": "9a94366983ce32c7724fc92e3b544327d4adb9be",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/268d5b2b4237c0abf76c4aa9633ad8580be01e1e",
"reference": "268d5b2b4237c0abf76c4aa9633ad8580be01e1e",
"shasum": ""
},
"require": {
@ -8305,28 +8304,28 @@
"ext-reflection": "*",
"ext-simplexml": "*",
"php": "^7.3 || ^8.0",
"phpunit/php-code-coverage": "^9.2.5",
"phpunit/php-code-coverage": "^9.2.6",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-timer": "^5.0.3",
"phpunit/phpunit": "^9.5.1",
"phpunit/phpunit": "^9.5.4",
"sebastian/environment": "^5.1.3",
"symfony/console": "^4.4 || ^5.2",
"symfony/process": "^4.4 || ^5.2"
"symfony/console": "^4.4.21 || ^5.2.6",
"symfony/process": "^4.4.21 || ^5.2.4"
},
"require-dev": {
"doctrine/coding-standard": "^8.2.0",
"ekino/phpstan-banned-code": "^0.3.1",
"doctrine/coding-standard": "^9.0.0",
"ekino/phpstan-banned-code": "^0.4.0",
"ergebnis/phpstan-rules": "^0.15.3",
"ext-posix": "*",
"infection/infection": "^0.20.2",
"phpstan/phpstan": "^0.12.70",
"infection/infection": "^0.21.5",
"phpstan/phpstan": "^0.12.84",
"phpstan/phpstan-deprecation-rules": "^0.12.6",
"phpstan/phpstan-phpunit": "^0.12.17",
"phpstan/phpstan-phpunit": "^0.12.18",
"phpstan/phpstan-strict-rules": "^0.12.9",
"squizlabs/php_codesniffer": "^3.5.8",
"symfony/filesystem": "^5.2.2",
"squizlabs/php_codesniffer": "^3.6.0",
"symfony/filesystem": "^5.2.6",
"thecodingmachine/phpstan-strict-rules": "^0.12.1",
"vimeo/psalm": "^4.4.1"
"vimeo/psalm": "^4.7.1"
},
"bin": [
"bin/paratest"
@ -8347,8 +8346,12 @@
{
"name": "Brian Scaturro",
"email": "scaturrob@gmail.com",
"homepage": "http://brianscaturro.com",
"role": "Lead"
"role": "Developer"
},
{
"name": "Filippo Tessarotto",
"email": "zoeslam@gmail.com",
"role": "Developer"
}
],
"description": "Parallel testing for PHP",
@ -8361,9 +8364,19 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v6.2.0"
"source": "https://github.com/paratestphp/paratest/tree/v6.3.0"
},
"time": "2021-01-29T15:25:31+00:00"
"funding": [
{
"url": "https://github.com/sponsors/Slamdunk",
"type": "github"
},
{
"url": "https://paypal.me/filippotessarotto",
"type": "paypal"
}
],
"time": "2021-04-27T09:24:27+00:00"
},
{
"name": "doctrine/instantiator",
@ -8501,16 +8514,16 @@
},
{
"name": "facade/ignition",
"version": "2.8.3",
"version": "2.8.4",
"source": {
"type": "git",
"url": "https://github.com/facade/ignition.git",
"reference": "a8201d51aae83addceaef9344592a3b068b5d64d"
"reference": "87fb348dab0ae1a7c206c3e902a5a44ba541742f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/facade/ignition/zipball/a8201d51aae83addceaef9344592a3b068b5d64d",
"reference": "a8201d51aae83addceaef9344592a3b068b5d64d",
"url": "https://api.github.com/repos/facade/ignition/zipball/87fb348dab0ae1a7c206c3e902a5a44ba541742f",
"reference": "87fb348dab0ae1a7c206c3e902a5a44ba541742f",
"shasum": ""
},
"require": {
@ -8574,7 +8587,7 @@
"issues": "https://github.com/facade/ignition/issues",
"source": "https://github.com/facade/ignition"
},
"time": "2021-04-09T20:45:59+00:00"
"time": "2021-04-29T13:55:26+00:00"
},
{
"name": "facade/ignition-contracts",
@ -8631,16 +8644,16 @@
},
{
"name": "filp/whoops",
"version": "2.12.0",
"version": "2.12.1",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "d501fd2658d55491a2295ff600ae5978eaad7403"
"reference": "c13c0be93cff50f88bbd70827d993026821914dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/d501fd2658d55491a2295ff600ae5978eaad7403",
"reference": "d501fd2658d55491a2295ff600ae5978eaad7403",
"url": "https://api.github.com/repos/filp/whoops/zipball/c13c0be93cff50f88bbd70827d993026821914dd",
"reference": "c13c0be93cff50f88bbd70827d993026821914dd",
"shasum": ""
},
"require": {
@ -8690,7 +8703,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.12.0"
"source": "https://github.com/filp/whoops/tree/2.12.1"
},
"funding": [
{
@ -8698,7 +8711,7 @@
"type": "github"
}
],
"time": "2021-03-30T12:00:00+00:00"
"time": "2021-04-25T12:00:00+00:00"
},
{
"name": "fzaninotto/faker",

View file

@ -2,7 +2,7 @@
return [
'description' => env('INSTANCE_DESCRIPTION', null),
'description' => env('INSTANCE_DESCRIPTION', 'Pixelfed - Photo sharing for everyone'),
'contact' => [
'enabled' => env('INSTANCE_CONTACT_FORM', false),
@ -18,7 +18,7 @@ return [
'is_public' => env('INSTANCE_PUBLIC_HASHTAGS', false)
],
],
'email' => env('INSTANCE_CONTACT_EMAIL'),
'timeline' => [
@ -72,4 +72,4 @@ return [
'org' => env('COVID_LABEL_ORG', 'visit the WHO website')
]
],
];
];

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddActiveToStoriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('stories', function (Blueprint $table) {
$table->boolean('active')->nullable()->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('stories', function (Blueprint $table) {
$table->dropColumn('active');
});
}
}

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/css/appdark.css vendored

Binary file not shown.

BIN
public/css/landing.css vendored

Binary file not shown.

BIN
public/js/activity.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/direct.js vendored

Binary file not shown.

BIN
public/js/hashtag.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/rempos.js vendored

Binary file not shown.

BIN
public/js/rempro.js vendored

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -89,6 +89,7 @@
<a v-if="!pageLoading && (page > 1 && page <= 2) || (page == 1 && ids.length != 0) || page == 'cropPhoto'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="nextPage">Next</a>
<a v-if="!pageLoading && page == 3" class="font-weight-bold text-decoration-none" href="#" @click.prevent="compose()">Post</a>
<a v-if="!pageLoading && page == 'addText'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="composeTextPost()">Post</a>
<a v-if="!pageLoading && page == 'video-2'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="compose()">Post</a>
</span>
</div>
</div>
@ -97,18 +98,20 @@
<div class="list-group list-group-flush">
<div
v-for="(item, index) in availableLicenses"
class="list-group-item cursor-pointer"
:class="{
'text-primary': licenseIndex === index,
class="list-group-item cursor-pointer"
:class="{
'text-primary': licenseIndex === index,
'font-weight-bold': licenseIndex === index
}"
}"
@click="toggleLicense(index)">
{{item.name}}
</div>
</div>
</div>
<div v-if="page == 'textOptions'" class="w-100 h-100" style="min-height: 280px;">
</div>
<div v-if="page == 'addText'" class="w-100 h-100" style="min-height: 280px;">
<div class="mt-2">
<div class="media px-3">
@ -148,10 +151,10 @@
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;background-color: #008DF5">
<i class="fas fa-bolt text-white fa-lg"></i>
</div>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Post</span>
<span class="h5 mt-0 font-weight-bold text-primary">New Post</span>
</p>
<p class="mb-0 text-muted">Share up to {{config.uploader.album_limit}} photos or videos</p>
</div>
@ -164,7 +167,7 @@
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;border: 2px solid #008DF5">
<i class="far fa-edit text-primary fa-lg"></i>
</div>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Text Post</span>
@ -183,10 +186,10 @@
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;border: 2px solid #008DF5">
<i class="fas fa-history text-primary fa-lg"></i>
</div>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Story</span>
<span class="h5 mt-0 font-weight-bold text-primary">New Story</span>
<sup class="float-right mt-2">
<span class="btn btn-outline-lighter p-1 btn-sm font-weight-bold py-0" style="font-size:10px;line-height: 0.6">BETA</span>
</sup>
@ -202,10 +205,10 @@
<div class="media">
<div class="mr-3 align-items-center justify-content-center" style="display:inline-flex;width:40px;height:40px;border-radius: 100%;border: 2px solid #008DF5">
<i class="fas fa-images text-primary fa-lg"></i>
</div>
</div>
<div class="media-body text-left">
<p class="mb-0">
<span class="h5 mt-0 font-weight-bold text-primary">New Collection</span>
<span class="h5 mt-0 font-weight-bold text-primary">New Collection</span>
<sup class="float-right mt-2">
<span class="btn btn-outline-lighter p-1 btn-sm font-weight-bold py-0" style="font-size:10px;line-height: 0.6">BETA</span>
</sup>
@ -216,7 +219,7 @@
</div>
</a>
<p class="py-3">
<a class="font-weight-bold" href="/site/help">Help</a>
</p>
@ -268,7 +271,7 @@
<div class="nav-link" style="display:block;width:300px;height:300px;" @click="carouselCursor = i">
<!-- <img :class="'d-block img-fluid w-100 ' + [m.filter_class?m.filter_class:'']" :src="m.url" :alt="m.description" :title="m.description"> -->
<span :class="[m.filter_class?m.filter_class:'']">
<span :class="'rounded border ' + [i == carouselCursor ? ' border-primary shadow':'']" :style="'display:block;padding:5px;width:100%;height:100%;background-image: url(' + m.url + ');background-size:cover;border-width:3px !important;'"></span>
</span>
</div>
@ -372,7 +375,7 @@
</div>
<div v-if="page == 'tagPeople'" class="w-100 h-100 p-3">
<autocomplete
<autocomplete
v-show="taggedUsernames.length < 10"
:search="tagSearch"
placeholder="@pixelfed"
@ -413,7 +416,7 @@
<div v-if="page == 'addLocation'" class="w-100 h-100 p-3">
<p class="mb-0">Add Location</p>
<autocomplete
<autocomplete
:search="locationSearch"
placeholder="Search locations ..."
aria-label="Search locations ..."
@ -501,21 +504,21 @@
<div class="list-group list-group-flush">
<div
v-if="!profile.locked"
class="list-group-item lead cursor-pointer"
:class="{ 'text-primary': visibility == 'public' }"
class="list-group-item lead cursor-pointer"
:class="{ 'text-primary': visibility == 'public' }"
@click="toggleVisibility('public')">
Public
</div>
<div
v-if="!profile.locked"
class="list-group-item lead cursor-pointer"
:class="{ 'text-primary': visibility == 'unlisted' }"
class="list-group-item lead cursor-pointer"
:class="{ 'text-primary': visibility == 'unlisted' }"
@click="toggleVisibility('unlisted')">
Unlisted
</div>
<div
class="list-group-item lead cursor-pointer"
:class="{ 'text-primary': visibility == 'private' }"
<div
class="list-group-item lead cursor-pointer"
:class="{ 'text-primary': visibility == 'private' }"
@click="toggleVisibility('private')">
Followers Only
</div>
@ -589,7 +592,7 @@
<span>{{media[carouselCursor].license ? media[carouselCursor].license.length : 0}}/140</span>
</p> -->
<select class="form-control" v-model="licenseIndex">
<option
<option
v-for="(item, index) in availableLicenses"
:value="index"
:selected="index === licenseIndex">
@ -606,6 +609,69 @@
</p>
</div>
<div v-if="page == 'video-2'" class="w-100 h-100">
<div v-if="video.title.length" class="border-bottom">
<div class="media p-3">
<img :src="media[0].url" width="100px" height="70px" :class="[media[0].filter_class?'mr-2 ' + media[0].filter_class:'mr-2']">
<div class="media-body">
<p class="font-weight-bold mb-1">{{video.title ? video.title.slice(0,70) : 'Untitled'}}</p>
<p class="mb-0 text-muted small">{{video.description ? video.description.slice(0,90) : 'No description'}}</p>
</div>
</div>
</div>
<div class="border-bottom d-flex justify-content-between px-4 mb-0 py-2 ">
<div>
<div class="text-dark ">Contains NSFW Media</div>
</div>
<div>
<div class="custom-control custom-switch" style="z-index: 9999;">
<input type="checkbox" class="custom-control-input" id="asnsfw" v-model="nsfw">
<label class="custom-control-label" for="asnsfw"></label>
</div>
</div>
</div>
<div class="border-bottom">
<p class="px-4 mb-0 py-2 cursor-pointer" @click="showLicenseCard()">Add license</p>
</div>
<div class="border-bottom">
<p class="px-4 mb-0 py-2">
<span>Audience</span>
<span class="float-right">
<a href="#" @click.prevent="showVisibilityCard()" class="btn btn-outline-secondary btn-sm small mr-3 mt-n1 disabled" style="font-size:10px;padding:3px;text-transform: uppercase" disabled>{{visibilityTag}}</a>
<a href="#" @click.prevent="showVisibilityCard()" class="text-decoration-none"><i class="fas fa-chevron-right fa-lg text-lighter"></i></a>
</span>
</p>
</div>
<div class="p-3">
<!-- <div class="card card-body shadow-none border d-flex justify-content-center align-items-center mb-3 p-5">
<div class="d-flex align-items-center">
<p class="mb-0 text-center">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</p>
<p class="ml-3 mb-0 text-center font-weight-bold">
Processing video
</p>
</div>
</div> -->
<div class="form-group">
<p class="small font-weight-bold text-muted mb-0">Title</p>
<input class="form-control" v-model="video.title" placeholder="Add a good title">
<p class="help-text mb-0 small text-muted">{{video.title.length}}/70</p>
</div>
<div class="form-group mb-0">
<p class="small font-weight-bold text-muted mb-0">Description</p>
<textarea class="form-control" v-model="video.description" placeholder="Add an optional description" maxlength="5000" rows="5"></textarea>
<p class="help-text mb-0 small text-muted">{{video.description.length}}/5000</p>
</div>
</div>
</div>
</div>
<!-- card-footers -->
@ -679,7 +745,7 @@ import VueTribute from 'vue-tribute'
export default {
components: {
components: {
VueCropper,
Autocomplete,
VueTribute
@ -700,12 +766,16 @@ export default {
carouselCursor: 0,
uploading: false,
uploadProgress: 100,
composeType: false,
mode: 'photo',
modes: [
'photo',
'video',
'plain'
],
page: 1,
composeState: 'publish',
visibility: 'public',
visibilityTag: 'Public',
nsfw: false,
place: false,
commentsDisabled: false,
optimizeMedia: true,
@ -810,15 +880,16 @@ export default {
name: "Attribution-NonCommercial-NoDerivs"
}
],
licenseIndex: 0
licenseIndex: 0,
video: {
title: '',
description: ''
}
}
},
beforeMount() {
this.fetchProfile();
if(this.config.uploader.media_types.includes('video/mp4') == false) {
this.composeType = 'post'
}
this.filters = window.App.util.filters;
},
@ -860,6 +931,7 @@ export default {
this.pageTitle = 'New Text Post';
this.page = 'addText';
this.textMode = true;
this.mode = 'text';
},
mediaWatcher() {
@ -910,7 +982,14 @@ export default {
self.media.push(e.data);
self.uploading = false;
setTimeout(function() {
self.page = 2;
// if(type === 'video/mp4') {
// self.pageTitle = 'Edit Video Details';
// self.mode = 'video';
// self.page = 'video-2';
// } else {
// self.page = 2;
// }
self.page = 3;
}, 300);
}).catch(function(e) {
switch(e.response.status) {
@ -951,7 +1030,7 @@ export default {
return;
}
let id = this.media[this.carouselCursor].id;
axios.delete('/api/compose/v0/media/delete', {
params: {
id: id
@ -1001,7 +1080,8 @@ export default {
place: this.place,
tagged: this.taggedUsernames,
optimize_media: this.optimizeMedia,
license: this.availableLicenses[this.licenseIndex].id
license: this.availableLicenses[this.licenseIndex].id,
video: this.video
};
axios.post('/api/compose/v0/publish', data)
.then(res => {
@ -1068,41 +1148,99 @@ export default {
},
closeModal() {
this.composeType = '';
$('#composeModal').modal('hide');
},
goBack() {
this.pageTitle = '';
switch(this.page) {
case 'addText':
this.page = 1;
switch(this.mode) {
case 'photo':
switch(this.page) {
case 'addText':
this.page = 1;
break;
case 'textOptions':
this.page = 'addText';
break;
case 'cropPhoto':
case 'editMedia':
this.page = 2;
break;
case 'tagPeopleHelp':
this.showTagCard();
break;
case 'licensePicker':
this.page = 3;
break;
case 'video-2':
this.page = 1;
break;
default:
this.namedPages.indexOf(this.page) != -1 ?
this.page = 3 : this.page--;
break;
}
break;
case 'textOptions':
this.page = 'addText';
break;
case 'video':
switch(this.page) {
case 'licensePicker':
this.page = 'video-2';
break;
case 'cropPhoto':
case 'editMedia':
this.page = 2;
break;
case 'video-2':
this.page = 'video-2';
break;
case 'tagPeopleHelp':
this.showTagCard();
break;
case 'licensePicker':
this.page = 3;
default:
this.page = 'video-2';
break;
}
break;
default:
this.namedPages.indexOf(this.page) != -1 ?
this.page = (this.textMode ? 'addText' : 3) :
(this.textMode ? 'addText' : this.page--);
switch(this.page) {
case 'addText':
this.page = 1;
break;
case 'textOptions':
this.page = 'addText';
break;
case 'cropPhoto':
case 'editMedia':
this.page = 2;
break;
case 'tagPeopleHelp':
this.showTagCard();
break;
case 'licensePicker':
this.page = 3;
break;
case 'video-2':
this.page = 1;
break;
default:
this.namedPages.indexOf(this.page) != -1 ?
this.page = (this.mode == 'text' ? 'addText' : 3) :
(this.mode == 'text' ? 'addText' : this.page--);
break;
}
break;
}
},
nextPage() {
@ -1115,7 +1253,7 @@ export default {
case 'cropPhoto':
this.pageLoading = true;
let self = this;
this.$refs.cropper.getCroppedCanvas({
this.$refs.cropper.getCroppedCanvas({
maxWidth: 4096,
maxHeight: 4096,
fillColor: '#fff',
@ -1199,8 +1337,22 @@ export default {
onSubmitLocation(result) {
this.place = result;
this.pageTitle = this.textMode ? 'New Text Post' : '';
this.page = (this.textMode ? 'addText' : 3);
switch(this.mode) {
case 'photo':
this.pageTitle = '';
this.page = 3;
break;
case 'video':
this.pageTitle = 'Edit Video Details';
this.page = 'video-2';
break;
case 'text':
this.pageTitle = 'New Text Post';
this.page = 'addText';
break;
}
return;
},
@ -1227,8 +1379,23 @@ export default {
}
this.visibility = state;
this.visibilityTag = tags[state];
this.pageTitle = '';
this.page = this.textMode ? 'addText' : 3;
switch(this.mode) {
case 'photo':
this.pageTitle = '';
this.page = 3;
break;
case 'video':
this.pageTitle = 'Edit Video Details';
this.page = 'video-2';
break;
case 'text':
this.pageTitle = 'New Text Post';
this.page = 'addText';
break;
}
},
showMediaDescriptionsCard() {
@ -1350,9 +1517,24 @@ export default {
toggleLicense(index) {
this.licenseIndex = index;
this.pageTitle = '';
this.page = 3;
switch(this.mode) {
case 'photo':
this.pageTitle = '';
this.page = 3;
break;
case 'video':
this.pageTitle = 'Edit Video Details';
this.page = 'video-2';
break;
case 'text':
this.pageTitle = 'New Text Post';
this.page = 'addText';
break;
}
},
}
}
</script>
</script>

View file

@ -1,60 +1,92 @@
<template>
<div class="container mt-2 mt-md-5">
<div class="container mt-2 mt-md-5 bg-black">
<input type="file" id="pf-dz" name="media" class="d-none file-input" v-bind:accept="config.mimes">
<span class="fixed-top text-right m-3 cursor-pointer" @click="navigateTo()">
<i class="fas fa-times fa-lg text-white"></i>
</span>
<div v-if="loaded" class="row">
<div class="col-12 col-md-6 offset-md-3">
<div class="col-12 col-md-6 offset-md-3 bg-dark rounded-lg">
<!-- LANDING -->
<div v-if="page == 'landing'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 90vh;">
<div class="text-center flex-fill mt-5 pt-5">
<img src="/img/pixelfed-icon-grey.svg" width="60px" height="60px">
<p class="font-weight-bold lead text-lighter mt-1">Stories</p>
<!-- <p v-if="loaded" class="font-weight-bold small text-uppercase text-muted">
<span>{{stories.length}} Active</span>
<span class="px-2">|</span>
<span>30K Views</span>
</p> -->
<div class="text-center flex-fill pt-3">
<p class="text-muted font-weight-light mb-1">
<i class="fas fa-history fa-5x"></i>
</p>
<p class="text-muted font-weight-bold mb-0">STORIES</p>
</div>
<div class="flex-fill py-4">
<div class="card w-100 shadow-none">
<div class="list-group">
<div class="card w-100 shadow-none bg-transparent">
<div class="list-group bg-transparent">
<!-- <a class="list-group-item text-center lead text-decoration-none text-dark" href="#">Camera</a> -->
<a class="list-group-item text-center lead text-decoration-none text-dark" href="#" @click.prevent="upload()">Add Photo</a>
<a v-if="stories.length" class="list-group-item text-center lead text-decoration-none text-dark" href="#" @click.prevent="edit()">Edit</a>
<a class="list-group-item bg-transparent lead text-decoration-none text-light font-weight-bold border-light" href="#" @click.prevent="upload()">
<i class="fas fa-plus-square mr-2"></i>
Add to Story
</a>
<a v-if="stories.length" class="list-group-item bg-transparent lead text-decoration-none text-lighter font-weight-bold border-muted" href="#" @click.prevent="edit()">
<i class="far fa-clone mr-2"></i>
My Story
</a>
<a v-if="stories.length" class="list-group-item bg-transparent lead text-decoration-none text-lighter font-weight-bold border-muted" href="#" @click.prevent="viewMyStory()">
<i class="fas fa-history mr-2"></i>
View My Story
</a>
<!-- <a v-if="stories.length" class="list-group-item bg-transparent lead text-decoration-none text-lighter font-weight-bold border-muted" href="#" @click.prevent="edit()">
<i class="fas fa-network-wired mr-1"></i>
Audience
</a> -->
<!-- <a v-if="stories.length" class="list-group-item bg-transparent lead text-decoration-none text-lighter font-weight-bold border-muted" href="#" @click.prevent="edit()">
<i class="far fa-chart-bar mr-2"></i>
Stats
</a> -->
<!-- <a class="list-group-item bg-transparent lead text-decoration-none text-lighter font-weight-bold border-muted" href="#" @click.prevent="edit()">
<i class="far fa-folder mr-2"></i>
Archived
</a> -->
<!-- <a class="list-group-item bg-transparent lead text-decoration-none text-lighter font-weight-bold border-muted" href="#" @click.prevent="edit()">
<i class="far fa-question-circle mr-2"></i>
Help
</a> -->
<a class="list-group-item bg-transparent lead text-decoration-none text-lighter font-weight-bold border-muted" href="/">
<i class="fas fa-arrow-left mr-2"></i>
Go back
</a>
<!-- <a class="list-group-item text-center lead text-decoration-none text-dark" href="#">Options</a> -->
</div>
</div>
</div>
<div class="text-center flex-fill">
<p class="text-lighter small text-uppercase">
<!-- <p class="text-lighter small text-uppercase">
<a href="/" class="text-muted font-weight-bold">Home</a>
<span class="px-2 text-lighter">|</span>
<a href="/i/my/story" class="text-muted font-weight-bold">View My Story</a>
<span class="px-2 text-lighter">|</span>
<a href="/site/help" class="text-muted font-weight-bold">Help</a>
</p>
</p> -->
</div>
</div>
<!-- CROP -->
<div v-if="page == 'crop'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 95vh;">
<div class="text-center pt-5 mb-3 d-flex justify-content-between align-items-center">
<div v-if="page == 'crop'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 90vh;">
<div class="text-center py-3 d-flex justify-content-between align-items-center">
<div>
<button class="btn btn-outline-lighter btn-sm py-0 px-md-3"><i class="pr-2 fas fa-chevron-left fa-sm"></i> Delete</button>
<button class="btn btn-outline-lighter btn-sm py-1 px-md-3" @click="deleteCurrentStory()"><i class="pr-2 fas fa-chevron-left fa-sm"></i> Delete</button>
</div>
<div class="d-flex align-items-center">
<img class="d-inline-block mr-2" src="/img/pixelfed-icon-grey.svg" width="30px" height="30px">
<span class="font-weight-bold lead text-lighter">Stories</span>
<div class="">
<p class="text-muted font-weight-light mb-1">
<i class="fas fa-history fa-5x"></i>
</p>
<p class="text-muted font-weight-bold mb-0">STORIES</p>
</div>
<div>
<button class="btn btn-outline-success btn-sm py-0 px-md-3">Crop <i class="pl-2 fas fa-chevron-right fa-sm"></i></button>
<button class="btn btn-primary btn-sm py-1 px-md-3" @click="performCrop()">Crop <i class="pl-2 fas fa-chevron-right fa-sm"></i></button>
</div>
</div>
<div class="flex-fill">
<div class="card w-100 mt-3">
<div class="card-body p-0">
<vue-cropper
ref="cropper"
ref="croppa"
:relativeZoom="cropper.zoom"
:aspectRatio="cropper.aspectRatio"
:viewMode="cropper.viewMode"
@ -66,20 +98,11 @@
</div>
</div>
</div>
<div class="text-center flex-fill">
<p class="text-lighter small text-uppercase pt-2">
<!-- <a href="#" class="text-muted font-weight-bold">Home</a>
<span class="px-2 text-lighter">|</span>
<a href="#" class="text-muted font-weight-bold">View My Story</a>
<span class="px-2 text-lighter">|</span> -->
<a href="/site/help" class="text-muted font-weight-bold mb-0">Help</a>
</p>
</div>
</div>
<!-- ERROR -->
<div v-if="page == 'error'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center align-items-center" style="height: 90vh;">
<p class="h3 mb-0">Oops!</p>
<p class="h3 mb-0 text-light">Oops!</p>
<p class="text-muted lead">An error occurred, please try again later.</p>
<p class="text-muted mb-0">
<a class="btn btn-outline-secondary py-0 px-5 font-weight-bold" href="/">Go back</a>
@ -88,30 +111,63 @@
<!-- UPLOADING -->
<div v-if="page == 'uploading'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center align-items-center" style="height: 90vh;">
<p v-if="uploadProgress != 100" class="display-4 mb-0">Uploading {{uploadProgress}}%</p>
<p v-else class="display-4 mb-0">Publishing Story</p>
<p v-if="uploadProgress != 100" class="display-4 mb-0 text-muted">Uploading {{uploadProgress}}%</p>
<p v-else class="display-4 mb-0 text-muted">Processing ...</p>
</div>
<div v-if="page == 'edit'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 90vh;">
<div class="text-center flex-fill mt-5 pt-5">
<img src="/img/pixelfed-icon-grey.svg" width="60px" height="60px">
<p class="font-weight-bold lead text-lighter mt-1">Stories</p>
<!-- CROPPING -->
<div v-if="page == 'cropping'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center align-items-center" style="height: 90vh;">
<p class="display-4 mb-0 text-muted">Cropping ...</p>
</div>
<!-- PREVIEW -->
<div v-if="page == 'preview'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center align-items-center" style="height: 90vh;">
<div>
<div class="form-group">
<label for="durationSlider" class="text-light lead font-weight-bold">Story Duration</label>
<input type="range" class="custom-range" min="3" max="10" id="durationSlider" v-model="duration">
<p class="help-text text-center">
<span class="text-light">{{duration}} seconds</span>
</p>
</div>
<hr class="my-3">
<a class="btn btn-primary btn-block px-5 font-weight-bold my-3" href="#" @click.prevent="shareStoryToFollowers()">
Share Story with followers
</a>
<a class="btn btn-outline-muted btn-block px-5 font-weight-bold" href="/" @click.prevent="deleteCurrentStory()">
Cancel
</a>
</div>
<div class="flex-fill py-5">
<div class="card w-100 shadow-none" style="max-height: 500px; overflow-y: auto">
<!-- <a class="btn btn-outline-secondary btn-block px-5 font-weight-bold" href="#">
Share Story with everyone
</a> -->
</div>
<!-- EDIT -->
<div v-if="page == 'edit'" class="card card-body bg-transparent border-0 shadow-none d-flex justify-content-center" style="height: 90vh;">
<div class="text-center flex-fill mt-5">
<p class="text-muted font-weight-light mb-1">
<i class="fas fa-history fa-5x"></i>
</p>
<p class="text-muted font-weight-bold mb-0">STORIES</p>
</div>
<div class="flex-fill py-4">
<p class="lead font-weight-bold text-lighter">My Stories</p>
<div class="card w-100 shadow-none bg-transparent" style="max-height: 50vh; overflow-y: scroll">
<div class="list-group">
<div v-for="(story, index) in stories" class="list-group-item text-center text-dark" href="#">
<div v-for="(story, index) in stories" class="list-group-item bg-transparent text-center border-muted text-lighter" href="#">
<div class="media align-items-center">
<div class="mr-3 cursor-pointer" @click="showLightbox(story)">
<img :src="story.src" class="img-fluid" width="70px" height="70px">
<p class="small text-muted text-center mb-0">(expand)</p>
<img :src="story.src" class="rounded-circle border" width="40px" height="40px" style="object-fit: cover;">
</div>
<div class="media-body">
<p class="mb-0">Expires</p>
<p class="mb-0 text-muted small"><span>{{expiresTimestamp(story.expires_at)}}</span></p>
<div class="media-body text-left">
<p class="mb-0 text-muted font-weight-bold"><span>{{story.created_ago}} ago</span></p>
</div>
<div class="float-right">
<button @click="deleteStory(story, index)" class="btn btn-danger btn-sm font-weight-bold text-uppercase">Delete</button>
<div class="flex-grow-1 text-right">
<button @click="deleteStory(story, index)" class="btn btn-link btn-sm">
<i class="fas fa-trash-alt fa-lg text-muted"></i>
</button>
</div>
</div>
</div>
@ -119,7 +175,7 @@
</div>
</div>
<div class="flex-fill text-center">
<a class="btn btn-outline-secondary py-0 px-5 font-weight-bold" href="/i/stories/new">Go back</a>
<a class="btn btn-outline-secondary btn-block px-5 font-weight-bold" href="/i/stories/new" @click.prevent="goBack()">Go back</a>
</div>
</div>
</div>
@ -130,18 +186,24 @@
hide-header
hide-footer
centered
size="lg"
body-class="p-0"
size="md"
class="bg-transparent"
body-class="p-0 bg-transparent"
>
<div v-if="lightboxMedia" class="w-100 h-100">
<img :src="lightboxMedia.url" style="max-height: 100%; max-width: 100%">
<div v-if="lightboxMedia" class="w-100 h-100 bg-transparent">
<img :src="lightboxMedia.url" style="max-height: 90vh; width: 100%; object-fit: contain;">
</div>
</b-modal>
</div>
</template>
<style type="text/css" scoped>
<style type="text/css">
.bg-black {
background-color: #262626;
}
#lightbox .modal-content {
background: transparent;
}
</style>
<script type="text/javascript">
@ -149,7 +211,7 @@
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
export default {
components: {
components: {
VueCropper,
VueTimeago
},
@ -182,12 +244,15 @@
zoom: null
},
mediaUrl: null,
mediaId: null,
stories: [],
lightboxMedia: false,
duration: 3
};
},
mounted() {
$('body').addClass('bg-black');
this.mediaWatcher();
axios.get('/api/stories/v0/fetch/' + this.profileId)
.then(res => {
@ -241,19 +306,22 @@
}
};
io.value = null;
axios.post('/api/stories/v0/add', form, xhrConfig)
.then(function(e) {
self.uploadProgress = 100;
self.uploading = false;
window.location.href = '/i/my/story';
self.mediaUrl = e.data.media_url;
self.mediaId = e.data.media_id;
self.page = e.data.media_type === 'video' ? 'preview' : 'crop';
// window.location.href = '/i/my/story';
}).catch(function(e) {
self.uploading = false;
io.value = null;
let msg = e.response.data.message ? e.response.data.message : 'Something went wrong.'
swal('Oops!', msg, 'warning');
self.page = 'error';
});
io.value = null;
self.uploadProgress = 0;
});
},
@ -286,8 +354,50 @@
window.location.href = '/i/stories/new';
}
});
},
navigateTo(path = '/') {
window.location.href = path;
},
goBack() {
this.page = 'landing';
},
performCrop() {
this.page = 'cropping';
let data = this.$refs.croppa.getData();
axios.post('/api/stories/v0/crop', {
media_id: this.mediaId,
width: data.width,
height: data.height,
x: data.x,
y: data.y
}).then(res => {
this.page = 'preview';
});
},
deleteCurrentStory() {
let story = {
id: this.mediaId
};
this.deleteStory(story);
this.page = 'landing';
},
shareStoryToFollowers() {
axios.post('/api/stories/v0/publish', {
media_id: this.mediaId,
duration: this.duration
}).then(res => {
window.location.href = '/i/my/story?id=' + this.mediaId;
})
},
viewMyStory() {
window.location.href = '/i/my/story';
}
}
}
</script>
</script>

View file

@ -230,6 +230,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::delete('v0/delete/{id}', 'StoryController@apiV1Delete')->middleware('throttle:maxStoryDeletePerDay,1440');
Route::get('v0/me', 'StoryController@apiV1Me');
Route::get('v0/item/{id}', 'StoryController@apiV1Item');
Route::post('v0/crop', 'StoryController@cropPhoto');
Route::post('v0/publish', 'StoryController@publishStory');
});
});