Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging

This commit is contained in:
mbliznikova 2023-10-23 18:46:11 +00:00
commit e3de4c3e68
84 changed files with 2957 additions and 886 deletions

View file

@ -4,6 +4,7 @@
### Added ### Added
- Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6)) - Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6))
- Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12))
- Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7)) - Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7))
- Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde)) - Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde))
@ -34,6 +35,8 @@
- Update CreateAvatar job, add processing constraints and set `is_remote` attribute ([319ced40](https://github.com/pixelfed/pixelfed/commit/319ced40)) - Update CreateAvatar job, add processing constraints and set `is_remote` attribute ([319ced40](https://github.com/pixelfed/pixelfed/commit/319ced40))
- Update RemoteStatusDelete and DecrementPostCount pipelines ([edbcf3ed](https://github.com/pixelfed/pixelfed/commit/edbcf3ed)) - Update RemoteStatusDelete and DecrementPostCount pipelines ([edbcf3ed](https://github.com/pixelfed/pixelfed/commit/edbcf3ed))
- Update lexer regex, fix mention regex and add more tests ([778e83d3](https://github.com/pixelfed/pixelfed/commit/778e83d3)) - Update lexer regex, fix mention regex and add more tests ([778e83d3](https://github.com/pixelfed/pixelfed/commit/778e83d3))
- Update StatusTransformer, generate autolink on request ([dfe2379b](https://github.com/pixelfed/pixelfed/commit/dfe2379b))
- Update ComposeModal component, fix multi filter bug and allow media re-ordering before upload/posting ([56e315f6](https://github.com/pixelfed/pixelfed/commit/56e315f6))
- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)

View file

@ -10,8 +10,11 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use App\Services\Media\MediaHlsService;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class MediaDeletePipeline implements ShouldQueue class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -20,8 +23,34 @@ class MediaDeletePipeline implements ShouldQueue
public $timeout = 300; public $timeout = 300;
public $tries = 3; public $tries = 3;
public $maxExceptions = 1; public $maxExceptions = 1;
public $failOnTimeout = true;
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'media:purge-job:id-' . $this->media->id;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping("media:purge-job:id-{$this->media->id}"))->shared()->dontRelease()];
}
public function __construct(Media $media) public function __construct(Media $media)
{ {
$this->media = $media; $this->media = $media;
@ -63,9 +92,17 @@ class MediaDeletePipeline implements ShouldQueue
$disk->delete($thumb); $disk->delete($thumb);
} }
if($media->hls_path != null) {
$files = MediaHlsService::allFiles($media);
if($files && count($files)) {
foreach($files as $file) {
$disk->delete($file);
}
}
}
$media->delete(); $media->delete();
return 1; return 1;
} }
} }

View file

@ -0,0 +1,109 @@
<?php
namespace App\Jobs\VideoPipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use FFMpeg\Format\Video\X264;
use FFMpeg;
use Cache;
use App\Services\MediaService;
use App\Services\StatusService;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class VideoHlsPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media;
public $timeout = 900;
public $tries = 3;
public $maxExceptions = 1;
public $failOnTimeout = true;
public $deleteWhenMissingModels = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'media:video-hls:id-' . $this->media->id;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping("media:video-hls:id-{$this->media->id}"))->shared()->dontRelease()];
}
/**
* Create a new job instance.
*/
public function __construct($media)
{
$this->media = $media;
}
/**
* Execute the job.
*/
public function handle(): void
{
$depCheck = Cache::rememberForever('video-pipeline:hls:depcheck', function() {
$bin = config('laravel-ffmpeg.ffmpeg.binaries');
$output = shell_exec($bin . ' -version');
if($output && preg_match('/ffmpeg version ([^\s]+)/', $output, $matches)) {
$version = $matches[1];
return (version_compare($version, config('laravel-ffmpeg.min_hls_version')) >= 0) ? 'ok' : false;
} else {
return false;
}
});
if(!$depCheck || $depCheck !== 'ok') {
return;
}
$media = $this->media;
$bitrate = (new X264)->setKiloBitrate(config('media.hls.bitrate') ?? 1000);
$mp4 = $media->media_path;
$man = str_replace('.mp4', '.m3u8', $mp4);
FFMpeg::fromDisk('local')
->open($mp4)
->exportForHLS()
->setSegmentLength(16)
->setKeyFrameInterval(48)
->addFormat($bitrate)
->save($man);
$media->hls_path = $man;
$media->hls_transcoded_at = now();
$media->save();
MediaService::del($media->status_id);
usleep(50000);
StatusService::del($media->status_id);
return;
}
}

View file

@ -16,13 +16,46 @@ use App\Jobs\MediaPipeline\MediaStoragePipeline;
use App\Util\Media\Blurhash; use App\Util\Media\Blurhash;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\StatusService; use App\Services\StatusService;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class VideoThumbnail implements ShouldQueue class VideoThumbnail implements ShouldQueue, ShouldBeUniqueUntilProcessing
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media; protected $media;
public $timeout = 900;
public $tries = 3;
public $maxExceptions = 1;
public $failOnTimeout = true;
public $deleteWhenMissingModels = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'media:video-thumb:id-' . $this->media->id;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping("media:video-thumb:id-{$this->media->id}"))->shared()->dontRelease()];
}
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -54,7 +87,7 @@ class VideoThumbnail implements ShouldQueue
$path[$i] = $t; $path[$i] = $t;
$save = implode('/', $path); $save = implode('/', $path);
$video = FFMpeg::open($base) $video = FFMpeg::open($base)
->getFrameFromSeconds(0) ->getFrameFromSeconds(1)
->export() ->export()
->toDisk('local') ->toDisk('local')
->save($save); ->save($save);
@ -68,6 +101,9 @@ class VideoThumbnail implements ShouldQueue
$media->save(); $media->save();
} }
if(config('media.hls.enabled')) {
VideoHlsPipeline::dispatch($media)->onQueue('mmo');
}
} catch (Exception $e) { } catch (Exception $e) {
} }

View file

@ -0,0 +1,27 @@
<?php
namespace App\Services\Media;
use Storage;
class MediaHlsService
{
public static function allFiles($media)
{
$path = $media->media_path;
if(!$path) { return; }
$parts = explode('/', $path);
$filename = array_pop($parts);
$dir = implode('/', $parts);
[$name, $ext] = explode('.', $filename);
$files = Storage::files($dir);
return collect($files)
->filter(function($p) use($dir, $name) {
return str_starts_with($p, $dir . '/' . $name);
})
->values()
->toArray();
}
}

View file

@ -18,7 +18,7 @@ class MediaService
public static function get($statusId) public static function get($statusId)
{ {
return Cache::remember(self::CACHE_KEY.$statusId, 86400, function() use($statusId) { return Cache::remember(self::CACHE_KEY.$statusId, 21600, function() use($statusId) {
$media = Media::whereStatusId($statusId)->orderBy('order')->get(); $media = Media::whereStatusId($statusId)->orderBy('order')->get();
if(!$media) { if(!$media) {
return []; return [];
@ -46,7 +46,8 @@ class MediaService
$media['orientation'], $media['orientation'],
$media['filter_name'], $media['filter_name'],
$media['filter_class'], $media['filter_class'],
$media['mime'] $media['mime'],
$media['hls_manifest']
); );
$media['type'] = $mime ? strtolower($mime[0]) : 'unknown'; $media['type'] = $mime ? strtolower($mime[0]) : 'unknown';

View file

@ -4,6 +4,7 @@ namespace App\Transformer\Api;
use App\Media; use App\Media;
use League\Fractal; use League\Fractal;
use Storage;
class MediaTransformer extends Fractal\TransformerAbstract class MediaTransformer extends Fractal\TransformerAbstract
{ {
@ -28,6 +29,10 @@ class MediaTransformer extends Fractal\TransformerAbstract
'blurhash' => $media->blurhash ?? 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay' 'blurhash' => $media->blurhash ?? 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay'
]; ];
if(config('media.hls.enabled') && $media->hls_transcoded_at != null && $media->hls_path) {
$res['hls_manifest'] = url(Storage::url($media->hls_path));
}
if($media->width && $media->height) { if($media->width && $media->height) {
$res['meta'] = [ $res['meta'] = [
'focus' => [ 'focus' => [

View file

@ -7,86 +7,100 @@ use Illuminate\Support\Str;
class Config { class Config {
const CACHE_KEY = 'api:site:configuration:_v0.8'; const CACHE_KEY = 'api:site:configuration:_v0.8';
public static function get() { public static function get() {
return Cache::remember(self::CACHE_KEY, 900, function() { return Cache::remember(self::CACHE_KEY, 900, function() {
return [ $hls = [
'version' => config('pixelfed.version'), 'enabled' => config('media.hls.enabled'),
'open_registration' => (bool) config_cache('pixelfed.open_registration'), ];
'uploader' => [ if(config('media.hls.enabled')) {
'max_photo_size' => (int) config('pixelfed.max_photo_size'), $hls = [
'max_caption_length' => (int) config('pixelfed.max_caption_length'), 'enabled' => true,
'max_altext_length' => (int) config('pixelfed.max_altext_length', 150), 'debug' => (bool) config('media.hls.debug'),
'album_limit' => (int) config_cache('pixelfed.max_album_length'), 'p2p' => (bool) config('media.hls.p2p'),
'image_quality' => (int) config_cache('pixelfed.image_quality'), 'p2p_debug' => (bool) config('media.hls.p2p_debug'),
'tracker' => config('media.hls.tracker'),
'ice' => config('media.hls.ice')
];
}
return [
'version' => config('pixelfed.version'),
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'uploader' => [
'max_photo_size' => (int) config('pixelfed.max_photo_size'),
'max_caption_length' => (int) config('pixelfed.max_caption_length'),
'max_altext_length' => (int) config('pixelfed.max_altext_length', 150),
'album_limit' => (int) config_cache('pixelfed.max_album_length'),
'image_quality' => (int) config_cache('pixelfed.image_quality'),
'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), 'max_collection_length' => (int) config('pixelfed.max_collection_length', 18),
'optimize_image' => (bool) config('pixelfed.optimize_image'), 'optimize_image' => (bool) config('pixelfed.optimize_image'),
'optimize_video' => (bool) config('pixelfed.optimize_video'), 'optimize_video' => (bool) config('pixelfed.optimize_video'),
'media_types' => config_cache('pixelfed.media_types'), 'media_types' => config_cache('pixelfed.media_types'),
'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [], 'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [],
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit') 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit')
], ],
'activitypub' => [ 'activitypub' => [
'enabled' => (bool) config_cache('federation.activitypub.enabled'), 'enabled' => (bool) config_cache('federation.activitypub.enabled'),
'remote_follow' => config('federation.activitypub.remoteFollow') 'remote_follow' => config('federation.activitypub.remoteFollow')
], ],
'ab' => config('exp'), 'ab' => config('exp'),
'site' => [ 'site' => [
'name' => config_cache('app.name'), 'name' => config_cache('app.name'),
'domain' => config('pixelfed.domain.app'), 'domain' => config('pixelfed.domain.app'),
'url' => config('app.url'), 'url' => config('app.url'),
'description' => config_cache('app.short_description') 'description' => config_cache('app.short_description')
], ],
'account' => [ 'account' => [
'max_avatar_size' => config('pixelfed.max_avatar_size'), 'max_avatar_size' => config('pixelfed.max_avatar_size'),
'max_bio_length' => config('pixelfed.max_bio_length'), 'max_bio_length' => config('pixelfed.max_bio_length'),
'max_name_length' => config('pixelfed.max_name_length'), 'max_name_length' => config('pixelfed.max_name_length'),
'min_password_length' => config('pixelfed.min_password_length'), 'min_password_length' => config('pixelfed.min_password_length'),
'max_account_size' => config('pixelfed.max_account_size') 'max_account_size' => config('pixelfed.max_account_size')
], ],
'username' => [ 'username' => [
'remote' => [ 'remote' => [
'formats' => config('instance.username.remote.formats'), 'formats' => config('instance.username.remote.formats'),
'format' => config('instance.username.remote.format'), 'format' => config('instance.username.remote.format'),
'custom' => config('instance.username.remote.custom') 'custom' => config('instance.username.remote.custom')
] ]
], ],
'features' => [ 'features' => [
'timelines' => [ 'timelines' => [
'local' => true, 'local' => true,
'network' => (bool) config('federation.network_timeline'), 'network' => (bool) config('federation.network_timeline'),
], ],
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
'stories' => (bool) config_cache('instance.stories.enabled'), 'stories' => (bool) config_cache('instance.stories.enabled'),
'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'), 'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
'import' => [ 'import' => [
'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'), 'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'),
'mastodon' => false, 'mastodon' => false,
'pixelfed' => false 'pixelfed' => false
], ],
'label' => [ 'label' => [
'covid' => [ 'covid' => [
'enabled' => (bool) config('instance.label.covid.enabled'), 'enabled' => (bool) config('instance.label.covid.enabled'),
'org' => config('instance.label.covid.org'), 'org' => config('instance.label.covid.org'),
'url' => config('instance.label.covid.url'), 'url' => config('instance.label.covid.url'),
] ]
] ],
] 'hls' => $hls
]; ]
}); ];
} });
}
public static function json() { public static function json() {
return json_encode(self::get(), JSON_FORCE_OBJECT); return json_encode(self::get(), JSON_FORCE_OBJECT);
} }
} }

View file

@ -3,8 +3,7 @@
return [ return [
'ffmpeg' => [ 'ffmpeg' => [
'binaries' => env('FFMPEG_BINARIES', 'ffmpeg'), 'binaries' => env('FFMPEG_BINARIES', 'ffmpeg'),
'threads' => env('FFMPEG_THREADS', false),
'threads' => 12, // set to false to disable the default 'threads' filter
], ],
'ffprobe' => [ 'ffprobe' => [
@ -18,4 +17,6 @@ return [
'temporary_files_root' => env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir()), 'temporary_files_root' => env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir()),
'temporary_files_encrypted_hls' => env('FFMPEG_TEMPORARY_ENCRYPTED_HLS', env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir())), 'temporary_files_encrypted_hls' => env('FFMPEG_TEMPORARY_ENCRYPTED_HLS', env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir())),
'min_hls_version' => env('FFMPEG_MIN_HLS_VERSION', '4.3.0'),
]; ];

View file

@ -22,5 +22,39 @@ return [
'resilient_mode' => env('ALT_PRI_ENABLED', false) || env('ALT_SEC_ENABLED', false), 'resilient_mode' => env('ALT_PRI_ENABLED', false) || env('ALT_SEC_ENABLED', false),
], ],
],
'hls' => [
/*
|--------------------------------------------------------------------------
| Enable HLS
|--------------------------------------------------------------------------
|
| Enable optional HLS support, required for video p2p support. Requires FFMPEG
| Disabled by default.
|
*/
'enabled' => env('MEDIA_HLS_ENABLED', false),
'debug' => env('MEDIA_HLS_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Enable Video P2P support
|--------------------------------------------------------------------------
|
| Enable optional video p2p support. Requires FFMPEG + HLS
| Disabled by default.
|
*/
'p2p' => env('MEDIA_HLS_P2P', false),
'p2p_debug' => env('MEDIA_HLS_P2P_DEBUG', false),
'bitrate' => env('MEDIA_HLS_BITRATE', 1000),
'tracker' => env('MEDIA_HLS_P2P_TRACKER', 'wss://tracker.webtorrent.dev'),
'ice' => env('MEDIA_HLS_P2P_ICE_SERVER', 'stun:stun.l.google.com:19302'),
] ]
]; ];

2081
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -34,9 +34,12 @@
}, },
"dependencies": { "dependencies": {
"@fancyapps/fancybox": "^3.5.7", "@fancyapps/fancybox": "^3.5.7",
"@hcaptcha/vue-hcaptcha": "^1.3.0",
"@peertube/p2p-media-loader-core": "^1.0.14",
"@peertube/p2p-media-loader-hlsjs": "^1.0.14",
"@trevoreyre/autocomplete-vue": "^2.2.0", "@trevoreyre/autocomplete-vue": "^2.2.0",
"@web3-storage/parse-link-header": "^3.1.0", "@web3-storage/parse-link-header": "^3.1.0",
"@zip.js/zip.js": "^2.7.14", "@zip.js/zip.js": "^2.7.24",
"animate.css": "^4.1.0", "animate.css": "^4.1.0",
"bigpicture": "^2.6.2", "bigpicture": "^2.6.2",
"blurhash": "^1.1.3", "blurhash": "^1.1.3",

Binary file not shown.

BIN
public/js/activity.js vendored

Binary file not shown.

BIN
public/js/admin.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/daci.chunk.b17a0b11877389d7.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/direct.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/discover.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/hashtag.js vendored

Binary file not shown.

BIN
public/js/home.chunk.351f55e9d09b6482.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/landing.js vendored

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

BIN
public/js/portfolio.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/post.chunk.74f8b1d1954f5d01.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/spa.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/stories.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.

View file

@ -31,13 +31,13 @@
*/ */
/*! /*!
* Cropper.js v1.5.13 * Cropper.js v1.6.1
* https://fengyuanchen.github.io/cropperjs * https://fengyuanchen.github.io/cropperjs
* *
* Copyright 2015-present Chen Fengyuan * Copyright 2015-present Chen Fengyuan
* Released under the MIT license * Released under the MIT license
* *
* Date: 2022-11-20T05:30:46.114Z * Date: 2023-09-17T03:44:19.860Z
*/ */
/*! /*!
@ -56,6 +56,20 @@
* Licensed under GPL 3. * Licensed under GPL 3.
*/ */
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*! /*!
* Vue.js v2.7.14 * Vue.js v2.7.14
* (c) 2014-2022 Evan You * (c) 2014-2022 Evan You
@ -63,14 +77,14 @@
*/ */
/*! /*!
* jQuery JavaScript Library v3.7.0 * jQuery JavaScript Library v3.7.1
* https://jquery.com/ * https://jquery.com/
* *
* Copyright OpenJS Foundation and other contributors * Copyright OpenJS Foundation and other contributors
* Released under the MIT license * Released under the MIT license
* https://jquery.org/license * https://jquery.org/license
* *
* Date: 2023-05-11T18:29Z * Date: 2023-08-28T13:37Z
*/ */
/*! /*!
@ -143,6 +157,18 @@ and limitations under the License.
/*! https://mths.be/punycode v1.4.1 by @mathias */ /*! https://mths.be/punycode v1.4.1 by @mathias */
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! queue-microtask. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! run-parallel. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-websocket. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/** /**
* vue-class-component v7.2.3 * vue-class-component v7.2.3
* (c) 2015-present Evan You * (c) 2015-present Evan You
@ -158,6 +184,23 @@ and limitations under the License.
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/ */
/**
* @license Apache-2.0
* Copyright 2018 Novage LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** /**
* filesize * filesize
* *

Binary file not shown.

View file

@ -12,7 +12,7 @@
</div> </div>
<div v-else-if="status.pf_type === 'video'" class="w-100"> <div v-else-if="status.pf_type === 'video'" class="w-100">
<video-presenter :status="status" v-on:togglecw="status.sensitive = false"></video-presenter> <video-player :status="status" :fixedHeight="fixedHeight" v-on:togglecw="status.sensitive = false" />
</div> </div>
<div v-else-if="status.pf_type === 'photo:album'" class="w-100"> <div v-else-if="status.pf_type === 'photo:album'" class="w-100">
@ -108,27 +108,11 @@
</div> </div>
</div> </div>
<template v-else-if="status.pf_type === 'video'"> <video-player
<div v-if="status.sensitive == true" class="content-label-wrapper"> v-else-if="status.pf_type === 'video'"
<div class="text-light content-label"> :status="status"
<p class="text-center"> :fixedHeight="fixedHeight"
<i class="far fa-eye-slash fa-2x"></i> />
</p>
<p class="h4 font-weight-bold text-center">
Sensitive Content
</p>
<p class="text-center py-2 content-label-text">
{{ status.spoiler_text ? status.spoiler_text : 'This post may contain sensitive content.'}}
</p>
<p class="mb-0">
<button @click="status.sensitive = false" class="btn btn-outline-light btn-block btn-sm font-weight-bold">See Post</button>
</p>
</div>
</div>
<video v-else class="card-img-top shadow" :class="{ fixedHeight: fixedHeight }" style="border-radius:15px;object-fit: contain;background-color: #000;" controls :poster="getPoster(status)">
<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
</video>
</template>
<div v-else-if="status.pf_type === 'photo:album'" class="card-img-top shadow" style="border-radius: 15px;"> <div v-else-if="status.pf_type === 'photo:album'" class="card-img-top shadow" style="border-radius: 15px;">
<photo-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="toggleContentWarning()" style="border-radius:15px !important;object-fit: contain;background-color: #000;overflow: hidden;" :class="{ fixedHeight: fixedHeight }"/> <photo-album-presenter :status="status" v-on:lightbox="toggleLightbox" v-on:togglecw="toggleContentWarning()" style="border-radius:15px !important;object-fit: contain;background-color: #000;overflow: hidden;" :class="{ fixedHeight: fixedHeight }"/>
@ -185,12 +169,14 @@
<script type="text/javascript"> <script type="text/javascript">
import BigPicture from 'bigpicture'; import BigPicture from 'bigpicture';
import ReadMore from './ReadMore.vue'; import ReadMore from './ReadMore.vue';
import VideoPlayer from './../../presenter/VideoPlayer.vue';
export default { export default {
props: ['status'], props: ['status'],
components: { components: {
"read-more": ReadMore, "read-more": ReadMore,
"video-player": VideoPlayer
}, },
data() { data() {

View file

@ -0,0 +1,198 @@
<template>
<div>
<div v-if="status.sensitive == true" class="content-label-wrapper">
<div class="text-light content-label">
<p class="text-center">
<i class="far fa-eye-slash fa-2x"></i>
</p>
<p class="h4 font-weight-bold text-center">
Sensitive Content
</p>
<p class="text-center py-2 content-label-text">
{{ status.spoiler_text ? status.spoiler_text : 'This post may contain sensitive content.'}}
</p>
<p class="mb-0">
<button @click="status.sensitive = false" class="btn btn-outline-light btn-block btn-sm font-weight-bold">See Post</button>
</p>
</div>
</div>
<template v-else>
<div v-if="!shouldPlay" class="content-label-wrapper" :style="{ background: `linear-gradient(rgba(0, 0, 0, 0.2),rgba(0, 0, 0, 0.8)),url(${getPoster(status)})`, backgroundSize: 'cover'}">
<div class="text-light content-label">
<p class="mb-0">
<button @click.prevent="handleShouldPlay" class="btn btn-link btn-block btn-sm font-weight-bold">
<i class="fas fa-play fa-5x text-white"></i>
</button>
</p>
</div>
</div>
<template v-else>
<video v-if="hasHls" ref="video" :class="{ fixedHeight: fixedHeight }" style="margin:0" playsinline controls autoplay="false" :poster="getPoster(status)">
</video>
<video v-else class="card-img-top shadow" :class="{ fixedHeight: fixedHeight }" style="border-radius:15px;object-fit: contain;background-color: #000;" autoplay="false" controls :poster="getPoster(status)">
<source :src="status.media_attachments[0].url" :type="status.media_attachments[0].mime">
</video>
</template>
</template>
</div>
</template>
<script type="text/javascript">
import Hls from 'hls.js';
import "plyr/dist/plyr.css";
import Plyr from 'plyr';
import { p2pml } from '@peertube/p2p-media-loader-core'
import { Engine, initHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs'
export default {
props: ['status', 'fixedHeight'],
data() {
return {
shouldPlay: false,
hasHls: undefined,
hlsConfig: window.App.config.features.hls,
liveSyncDurationCount: 7,
isHlsSupported: false,
isP2PSupported: false,
engine: undefined,
}
},
mounted() {
this.$nextTick(() => {
this.init();
})
},
methods: {
handleShouldPlay(){
this.shouldPlay = true;
this.isHlsSupported = this.hlsConfig.enabled && Hls.isSupported();
this.isP2PSupported = this.hlsConfig.enabled && this.hlsConfig.p2p && Engine.isSupported();
this.$nextTick(() => {
this.init();
})
},
init() {
if(!this.status.sensitive && this.status.media_attachments[0]?.hls_manifest && this.isHlsSupported) {
this.hasHls = true;
this.$nextTick(() => {
this.initHls();
})
} else {
this.hasHls = false;
}
},
initHls() {
let loader;
if(this.isP2PSupported) {
const config = {
loader: {
trackerAnnounce: [this.hlsConfig.tracker],
rtcConfig: {
iceServers: [
{
urls: [this.hlsConfig.ice]
}
],
}
}
};
var engine = new Engine(config);
if(this.hlsConfig.p2p_debug) {
engine.on("peer_connect", peer => console.log("peer_connect", peer.id, peer.remoteAddress));
engine.on("peer_close", peerId => console.log("peer_close", peerId));
engine.on("segment_loaded", (segment, peerId) => console.log("segment_loaded from", peerId ? `peer ${peerId}` : "HTTP", segment.url));
}
loader = engine.createLoaderClass();
} else {
loader = Hls.DefaultConfig.loader;
}
const video = this.$refs.video;
const source = this.status.media_attachments[0].hls_manifest;
const player = new Plyr(video, {
captions: {
active: true,
update: true,
},
});
const hls = new Hls({
liveSyncDurationCount: this.liveSyncDurationCount,
loader: loader,
});
let self = this;
initHlsJsPlayer(hls);
hls.loadSource(source);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function(event, data) {
if(this.hlsConfig.debug) {
console.log(event);
console.log(data);
}
const defaultOptions = {};
const availableQualities = hls.levels.map((l) => l.height)
if(this.hlsConfig.debug) {
console.log(availableQualities);
}
availableQualities.unshift(0);
defaultOptions.quality = {
default: 0,
options: availableQualities,
forced: true,
onChange: (e) => self.updateQuality(e),
}
defaultOptions.i18n = {
qualityLabel: {
0: 'Auto',
},
}
hls.on(Hls.Events.LEVEL_SWITCHED, function(event, data) {
var span = document.querySelector(".plyr__menu__container [data-plyr='quality'][value='0'] span")
if (hls.autoLevelEnabled) {
span.innerHTML = `Auto (${hls.levels[data.level].height}p)`
} else {
span.innerHTML = `Auto`
}
})
var player = new Plyr(video, defaultOptions);
});
},
updateQuality(newQuality) {
if (newQuality === 0) {
window.hls.currentLevel = -1;
} else {
window.hls.levels.forEach((level, levelIndex) => {
if (level.height === newQuality) {
if(this.hlsConfig.debug) {
console.log("Found quality match with " + newQuality);
}
window.hls.currentLevel = levelIndex;
}
});
}
},
getPoster(status) {
let url = status.media_attachments[0].preview_url;
if(url.endsWith('no-preview.jpg') || url.endsWith('no-preview.png')) {
return;
}
return url;
}
}
}
</script>

View file

@ -0,0 +1,800 @@
<template>
<div class="card-body">
<p class="lead text-center font-weight-bold">Sign-in with Mastodon</p>
<hr>
<template v-if="step === 1">
<div class="wrapper-mh">
<div class="flex-grow-1">
<p class="text-dark">Hello {{ initialData['_webfinger'] }},</p>
<p class="lead font-weight-bold">Welcome to Pixelfed!</p>
<p>You are moments away from joining our vibrant photo and video focused community with members from around the world.</p>
</div>
<p class="text-xs text-lighter">Your Mastodon account <strong>avatar</strong>, <strong>bio</strong>, <strong>display name</strong>, <strong>followed accounts</strong> and <strong>username</strong> will be imported to speed up the sign-up process. We will never post on your behalf, we only access your public profile data (avatar, bio, display name, followed accounts and username).</p>
</div>
</template>
<template v-else-if="step === 2">
<div class="wrapper-mh">
<div class="pt-3">
<div class="form-group has-float-label">
<input class="form-control form-control-lg" id="f_username" aria-describedby="f_username_help" v-model="username" autofocus/>
<label for="f_username">Username</label>
<p v-if="validUsername && !usernameError" id="f_username_help" class="text-xs text-success font-weight-bold mt-1 mb-0">Available</p>
<p v-else-if="!validUsername && !usernameError" id="f_username_help" class="text-xs text-danger font-weight-bold mt-1 mb-0">Username taken</p>
<p v-else-if="usernameError" id="f_username_help" class="text-xs text-danger font-weight-bold mt-1 mb-0">{{ usernameError }}</p>
</div>
</div>
<div class="pt-3">
<p class="text-sm font-weight-bold mb-1">Avatar</p>
<div class="border rounded-lg p-3 d-flex align-items-center justify-content-between gap-1">
<img v-if="form.importAvatar" :src="initialData.avatar" width="40" height="40" class="rounded-circle" />
<img v-else src="/storage/avatars/default.jpg" width="40" height="40" class="rounded-circle" />
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="customCheck1" v-model="form.importAvatar">
<label class="custom-control-label text-xs font-weight-bold" style="line-height: 24px;" for="customCheck1">Import my Mastodon avatar</label>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="step === 3">
<div class="wrapper-mh">
<div class="pt-3">
<div class="form-group has-float-label">
<input class="form-control form-control-lg" id="f_name" aria-describedby="f_name_help" v-model="form.display_name" />
<label for="f_name">Display Name</label>
<div id="f_name_help" class="text-xs text-muted mt-1">Your display name, shown on your profile. You can change this later.</div>
</div>
</div>
<div class="pt-3">
<div class="form-group has-float-label">
<textarea class="form-control" id="f_bio" aria-describedby="f_bio_help" rows="5" v-model="form.bio"></textarea>
<label for="f_bio">Bio</label>
<div id="f_bio_help" class="text-xs text-muted mt-1 d-flex justify-content-between align-items-center">
<div>Describe yourself, you can change this later.</div>
<div>{{ form.bio ? form.bio.length : 0 }}/500</div>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="step === 4">
<div class="wrapper-mh">
<div class="pt-3">
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<p class="font-weight-bold mb-0">Import accounts you follow</p>
<p class="text-muted text-xs mb-0">You can skip this step and follow accounts later</p>
</div>
<div style="min-width: 100px;text-align:right;">
<p v-if="following && selectedFollowing && following.length == selectedFollowing.length" class="mb-0"><a class="font-weight-bold text-xs text-danger" href="#" @click.prevent="handleFollowerUnselectAll()">Unselect All</a></p>
<p v-else class="mb-0"><a class="font-weight-bold text-xs" href="#" @click.prevent="handleFollowerSelectAll()">Select All</a></p>
</div>
</div>
<div v-if="!followingFetched" class="d-flex align-items-center justify-content-center limit-h">
<div class="w-100">
<instagram-loader></instagram-loader>
</div>
</div>
<div v-else class="list-group limit-h">
<div v-for="(account, idx) in following" class="list-group-item">
<div class="d-flex align-items-center" style="gap:8px;">
<div class="d-flex align-items-center" style="gap:5px;">
<div class="custom-control custom-checkbox">
<input
type="checkbox"
class="custom-control-input"
:value="account.url"
:id="'fac' + idx"
v-model="selectedFollowing"
@change="handleFollower($event, account)">
<label class="custom-control-label" :for="'fac' + idx"></label>
</div>
<img v-if="account.avatar" :src="account.avatar" width="34" height="34" class="rounded-circle" />
<img v-else src="/storage/avatars/default.jpg" width="34" height="34" class="rounded-circle" />
</div>
<div style="max-width: 70%">
<p class="font-weight-bold mb-0 text-truncate">&commat;{{account.username}}</p>
<p class="text-xs text-lighter mb-0 text-truncate">{{account.url.replace('https://', '')}}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="step === 5">
<div class="wrapper-mh">
<div class="pt-3">
<div class="pb-3">
<p class="font-weight-bold mb-0">We need a bit more info</p>
<p class="text-xs text-muted">Enter your email so you recover access to your account in the future</p>
</div>
<div class="form-group has-float-label">
<input class="form-control" id="f_email" aria-describedby="f_email_help" v-model="email" autofocus="autofocus" />
<label for="f_email">Email address</label>
<p v-if="email && validEmail && !emailError" id="f_email_help" class="text-xs text-success font-weight-bold mt-1 mb-0">Available</p>
<p v-else-if="email && !validEmail && !emailError" id="f_email_help" class="text-xs text-danger font-weight-bold mt-1 mb-0">Email already in use</p>
<p v-else-if="email && emailError" id="f_email_help" class="text-xs text-danger font-weight-bold mt-1 mb-0">{{ emailError }}</p>
<p v-else id="f_email_help" class="text-xs text-muted mt-1 mb-0">We'll never share your email with anyone else.</p>
</div>
</div>
<div v-if="email && email.length && validEmail" class="pt-3">
<div class="form-group has-float-label">
<input type="password" class="form-control" id="f_password" aria-describedby="f_password_help" autocomplete="new-password" v-model="password" autofocus="autofocus" />
<label for="f_password">Password</label>
<div id="f_password_help" class="text-xs text-muted">Use a memorable password that you don't use on other sites.</div>
</div>
</div>
<div v-if="password && password.length >= 8" class="pt-3">
<div class="form-group has-float-label">
<input type="password" class="form-control" id="f_passwordConfirm" aria-describedby="f_passwordConfirm_help" autocomplete="new-password" v-model="passwordConfirm" autofocus="autofocus" />
<label for="f_passwordConfirm">Confirm Password</label>
<div id="f_passwordConfirm_help" class="text-xs text-muted">Re-enter your password.</div>
</div>
</div>
</div>
</template>
<template v-else-if="step === 6">
<div class="wrapper-mh">
<div class="my-5">
<p class="lead text-center font-weight-bold mb-0">You're almost ready!</p>
<p class="text-center text-lighter text-xs">Confirm your email and other info</p>
</div>
<div class="card shadow-none border" style="border-radius: 1rem;">
<div class="card-body">
<div class="d-flex gap-1">
<img :src="initialData.avatar" width="90" height="90" class="rounded-circle">
<div>
<p class="lead font-weight-bold mb-n1">@{{username}}</p>
<p class="small font-weight-light text-muted mb-1">{{username}}@pixelfed.test</p>
<p class="text-xs mb-0 text-lighter">{{ form.bio.slice(0, 80) + '...' }}</p>
</div>
</div>
</div>
</div>
<div class="list-group mt-3" style="border-radius: 1rem;">
<div class="list-group-item d-flex align-items-center justify-content-between">
<div class="text-xs">Email</div>
<div class="font-weight-bold">{{ email }}</div>
</div>
<div class="list-group-item d-flex align-items-center justify-content-between">
<div class="text-xs">Following Imports</div>
<div class="font-weight-bold">{{ selectedFollowing ? selectedFollowing.length : 0 }}</div>
</div>
</div>
</div>
</template>
<template v-else-if="step === 7">
<div class="wrapper-mh">
<div class="w-100 d-flex flex-column gap-1">
<b-progress :value="submitProgress" :max="100" height="1rem" animated></b-progress>
<p class="text-center text-xs text-lighter">{{ submitMessage }}</p>
</div>
</div>
</template>
<hr>
<template v-if="step === 7">
<div class="d-flex align-items-center justify-content-center gap-1 mb-2">
<button class="btn btn-outline-primary font-weight-bold btn-block my-0" @click="handleBack()">Back</button>
<button class="btn btn-primary font-weight-bold btn-block my-0" @click="handleJoin()" disabled>Continue</button>
</div>
</template>
<template v-else>
<div class="d-flex align-items-center justify-content-center gap-1 mb-2">
<button v-if="step > 1" class="btn btn-outline-primary font-weight-bold btn-block my-0" :disabled="isSubmitting" @click="handleBack()">Back</button>
<button
v-if="step === 6"
class="btn btn-primary font-weight-bold btn-block my-0"
:disabled="isSubmitting"
@click="handleJoin()">
<b-spinner v-if="isSubmitting" small />
<span v-else>Continue</span>
</button>
<button v-else class="btn btn-primary font-weight-bold btn-block my-0" :disabled="canProceed()" @click="handleProceed()">Next</button>
</div>
<template v-if="isSubmitting ? false : step <= 6">
<hr>
<p class="text-center mb-0">
<a class="font-weight-bold" href="/login">Go back to login</a>
</p>
</template>
</template>
</div>
</template>
<script type="text/javascript">
import {debounce} from './../../../js/util/debounce.js';
import { InstagramLoader } from 'vue-content-loader';
export default {
props: {
initialData: {
type: Object
}
},
components: {
InstagramLoader,
},
data() {
return {
step: 1,
validUsername: false,
usernameError: undefined,
username: this.initialData.username,
email: undefined,
emailError: undefined,
validEmail: false,
password: undefined,
passwordConfirm: undefined,
passwordValid: false,
following: [],
followingFetched: false,
selectedFollowing: [],
form: {
importAvatar: true,
bio: this.stripTagsPreserveNewlines(this.initialData.note),
display_name: this.initialData.display_name,
},
isSubmitting: false,
submitProgress: 0,
submitMessage: 'Please wait...',
isImportingFollowing: false,
accountToId: [],
followingIds: [],
accessToken: undefined,
}
},
mounted() {
this.checkUsernameAvailability();
},
watch: {
username: debounce(function(username) {
this.checkUsernameAvailability();
}, 500),
email: debounce(function(email) {
this.checkEmailAvailability();
}, 500),
passwordConfirm: function(confirm) {
this.checkPasswordConfirm(confirm);
},
selectedFollowing: function(account) {
this.lookupSelected(account);
}
},
methods: {
checkPasswordConfirm(password) {
if(!this.password || !password) {
return;
}
this.passwordValid = password.trim() === this.password.trim();
},
handleBack() {
event.currentTarget.blur();
this.step--;
},
handleProceed() {
event.currentTarget.blur();
this.step++;
if(!this.followingFetched) {
this.fetchFollowing();
}
},
checkUsernameAvailability() {
axios.post('/auth/raw/mastodon/s/username-check', {
username: this.username
})
.then(res => {
if(res.data && res.data.hasOwnProperty('exists')) {
this.usernameError = undefined;
this.validUsername = res.data.exists == false;
}
})
.catch(err => {
this.usernameError = err.response.data.message;
})
},
checkEmailAvailability() {
axios.post('/auth/raw/mastodon/s/email-check', {
email: this.email
})
.then(res => {
if(!res.data) {
this.emailError = undefined;
this.validEmail = false;
return;
}
if(res.data && res.data.hasOwnProperty('banned') && res.data.banned) {
this.emailError = 'This email provider is not supported, please use a different email address.';
this.validEmail = false;
return;
}
if(res.data && res.data.hasOwnProperty('exists')) {
this.emailError = undefined;
this.validEmail = res.data.exists == false;
}
})
.catch(err => {
this.emailError = err.response.data.message;
})
},
canProceed() {
switch(this.step) {
case 1:
return false;
break;
case 2:
return (this.usernameError || !this.validUsername);
break;
case 3:
return false;
break;
case 4:
return false;
break;
case 5:
return (
!this.email ||
!this.validEmail ||
!this.password ||
!this.password.length ||
this.password.length < 8 ||
!this.passwordConfirm ||
!this.passwordConfirm.length ||
this.passwordConfirm.length < 8 ||
!this.passwordValid
);
break;
case 6:
break;
}
},
handleFollower(event, account) {
let state = event.target.checked;
if(state) {
if(this.selectedFollowing.indexOf(account.url) == -1) {
this.selectedFollowing.push(account.url)
}
} else {
this.selectedFollowing = this.selectedFollowing.filter(s => s !== account.url);
}
},
handleFollowerSelectAll() {
this.selectedFollowing = this.following.map(f => f.url);
},
handleFollowerUnselectAll() {
this.selectedFollowing = [];
},
lookupSelected(accounts) {
if(!accounts || !accounts.length) {
return;
}
for (var i = accounts.length - 1; i >= 0; i--) {
let acct = accounts[i];
if(!this.accountToId.map(a => a.url).includes(acct)) {
axios.post('/auth/raw/mastodon/s/account-to-id', {
account: acct
})
.then(res => {
this.accountToId.push({
id: res.data.id,
url: acct
})
})
}
}
},
fetchFollowing() {
axios.post('/auth/raw/mastodon/s/following')
.then(res => {
this.following = res.data.following;
this.followingFetched = true;
})
.finally(() => {
setTimeout(() => {
this.followingFetched = true;
}, 1000)
})
},
stripTagsPreserveNewlines(htmlString) {
const parser = new DOMParser();
const document = parser.parseFromString(htmlString, 'text/html');
const body = document.body;
let strippedString = '';
function traverse(element) {
const nodeName = element.nodeName.toLowerCase();
if (nodeName === 'p') {
strippedString += '\n';
} else if (nodeName === '#text') {
strippedString += element.textContent;
}
const childNodes = element.childNodes;
for (let i = 0; i < childNodes.length; i++) {
traverse(childNodes[i]);
}
}
traverse(body);
strippedString = strippedString.trim();
return strippedString;
},
handleJoin() {
this.isSubmitting = true;
this.step = 7;
this.submitProgress = 10;
axios.post('/auth/raw/mastodon/s/submit', {
email: this.email,
name: this.form.display_name,
password: this.password,
password_confirmation: this.passwordConfirm,
username: this.username,
})
.then(res => {
if(res.data.hasOwnProperty('token') && res.data.token) {
this.accessToken = res.data.token;
setTimeout(() => {
this.submitProgress = 20;
this.submitMessage = 'Claiming your username...';
this.storeBio();
}, 2000);
} else {
swal('Something went wrong', 'An unexpected error occured, please try again later');
}
})
},
storeBio() {
axios.post('/auth/raw/mastodon/s/store-bio', {
bio: this.form.bio
})
.then(res => {
this.submitProgress = 30;
this.submitMessage = 'Importing your bio...';
})
.finally(() => {
this.storeFollowing();
})
},
storeFollowing() {
this.submitProgress = 40;
this.submitMessage = 'Importing following accounts...';
if(!this.selectedFollowing || !this.selectedFollowing.length) {
setTimeout(() => {
this.iterateFollowing();
}, 500);
return;
}
let ids = this.selectedFollowing
.map(id => {
return this.accountToId.filter(ai => ai.url == id).map(ai => ai.id);
})
.flat()
.filter(r => r && r.length && typeof r === 'string')
this.followingIds = ids;
setTimeout(() => {
this.iterateFollowing();
}, 500);
// axios.post('/auth/raw/mastodon/s/store-following', {
// accounts: this.selectedFollowing
// })
// .then(res => {
// this.followingIds = res.data;
// this.submitProgress = 40;
// this.submitMessage = 'Importing following accounts...';
// setTimeout(() => {
// this.iterateFollowing();
// }, 1000);
// })
},
iterateFollowing() {
if(!this.followingIds || !this.followingIds.length) {
this.storeAvatar();
return;
}
let id = this.followingIds.pop();
return this.handleFollow(id);
},
handleFollow(id) {
const config = {
headers: { Authorization: `Bearer ${this.accessToken}` }
};
axios.post(`/api/v1/accounts/${id}/follow`, {}, config)
.then(res => {
})
.finally(() => {
this.iterateFollowing();
})
},
storeAvatar() {
this.submitProgress = 70;
this.submitMessage = 'Importing your avatar...';
if(this.form.importAvatar == false) {
this.submitProgress = 90;
this.submitMessage = 'Preparing your account...';
this.finishUp();
return;
}
axios.post('/auth/raw/mastodon/s/store-avatar', {
avatar_url: this.initialData.avatar
})
.then(res => {
this.submitProgress = 90;
this.submitMessage = 'Preparing your account...';
this.finishUp();
})
},
finishUp() {
this.submitProgress = 92;
this.submitMessage = 'Finishing up...';
axios.post('/auth/raw/mastodon/s/finish-up')
.then(() => {
this.$emit('setCanReload');
this.submitProgress = 95;
this.submitMessage = 'Logging you in...';
setTimeout(() => {
this.submitProgress = 100;
window.location.reload();
}, 5000)
})
}
}
}
</script>
<style lang="scss">
.wrapper-mh {
min-height: 429px;
display: flex;
justify-content: center;
flex-direction: column;
}
.limit-h {
height: 300px;
overflow-x: hidden;
overflow-y: auto;
}
.has-float-label {
display: block;
position: relative;
}
.has-float-label label, .has-float-label > span {
position: absolute;
left: 0;
top: 0;
cursor: text;
font-size: 75%;
font-weight: bold;
opacity: 1;
-webkit-transition: all .2s;
transition: all .2s;
top: -.5em;
left: 0.75rem;
z-index: 3;
line-height: 1;
padding: 0 4px;
background: #fff;
}
.has-float-label label::after, .has-float-label > span::after {
content: " ";
display: block;
position: absolute;
background: #fff;
height: 2px;
top: 50%;
left: -.2em;
right: -.2em;
z-index: -1;
}
.has-float-label .form-control::-webkit-input-placeholder {
opacity: 1;
-webkit-transition: all .2s;
transition: all .2s;
}
.has-float-label .form-control::-moz-placeholder {
opacity: 1;
transition: all .2s;
}
.has-float-label .form-control:-ms-input-placeholder {
opacity: 1;
transition: all .2s;
}
.has-float-label .form-control::placeholder {
opacity: 1;
-webkit-transition: all .2s;
transition: all .2s;
}
.has-float-label .form-control:placeholder-shown:not(:focus)::-webkit-input-placeholder {
opacity: 0;
}
.has-float-label .form-control:placeholder-shown:not(:focus)::-moz-placeholder {
opacity: 0;
}
.has-float-label .form-control:placeholder-shown:not(:focus):-ms-input-placeholder {
opacity: 0;
}
.has-float-label .form-control:placeholder-shown:not(:focus)::placeholder {
opacity: 0;
}
.has-float-label .form-control:placeholder-shown:not(:focus) + * {
font-size: 150%;
opacity: .5;
top: .3em;
}
.input-group .has-float-label {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1;
margin-bottom: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.input-group .has-float-label .form-control {
width: 100%;
border-radius: 0.25rem;
}
.input-group .has-float-label:not(:last-child), .input-group .has-float-label:not(:last-child) .form-control {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
border-right: 0;
}
.input-group .has-float-label:not(:first-child), .input-group .has-float-label:not(:first-child) .form-control {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.opacity-0 {
opacity: 0;
transition: opacity 0.5s;
}
.sl {
.progress {
background-color: #fff;
}
#tick {
stroke: #63bc01;
stroke-width: 6;
transition: all 1s;
}
#circle {
stroke: #63bc01;
stroke-width: 6;
transform-origin: 50px 50px 0;
transition: all 1s;
}
.progress #tick {
opacity: 0;
}
.ready #tick {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 8s ease-out forwards;
}
.progress #circle {
stroke: #4c4c4c;
stroke-dasharray: 314;
stroke-dashoffset: 1000;
animation: spin 3s linear infinite;
}
.ready #circle {
stroke-dashoffset: 66;
stroke: #63bc01;
}
#circle {
stroke-dasharray: 500;
}
@keyframes spin {
0% {
transform: rotate(0deg);
stroke-dashoffset: 66;
}
50% {
transform: rotate(540deg);
stroke-dashoffset: 314;
}
100% {
transform: rotate(1080deg);
stroke-dashoffset: 66;
}
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
#scheck {
width: 300px;
height: 300px;
}
}
</style>

View file

@ -178,9 +178,13 @@
</span> </span>
<span v-else> <span v-else>
<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 > 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> <template v-if="!pageLoading && page == 3" >
<b-spinner v-if="isPosting" small />
<a v-else class="font-weight-bold text-decoration-none" href="#" @click.prevent="compose()">Post</a>
</template>
<a v-if="!pageLoading && page == 'addText'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="composeTextPost()">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> <a v-if="!pageLoading && page == 'video-2'" class="font-weight-bold text-decoration-none" href="#" @click.prevent="compose()">Post</a>
<span v-if="!pageLoading && page == 'filteringMedia'" class="font-weight-bold text-decoration-none text-muted">Next</span>
</span> </span>
</div> </div>
</div> </div>
@ -201,10 +205,10 @@
</div> </div>
</div> </div>
<div v-if="page == 'textOptions'" class="w-100 h-100" style="min-height: 280px;"> <div v-else-if="page == 'textOptions'" class="w-100 h-100" style="min-height: 280px;">
</div> </div>
<div v-if="page == 'addText'" class="w-100 h-100" style="min-height: 280px;"> <div v-else-if="page == 'addText'" class="w-100 h-100" style="min-height: 280px;">
<div class="mt-2"> <div class="mt-2">
<div class="media px-3"> <div class="media px-3">
<div class="media-body"> <div class="media-body">
@ -236,7 +240,7 @@
</div> </div>
</div> </div>
<div v-if="page == 1" class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: 400px;"> <div v-else-if="page == 1" class="w-100 h-100 d-flex justify-content-center align-items-center" style="min-height: 400px;">
<div class="text-center"> <div class="text-center">
<div v-if="media.length == 0" class="card my-md-3 shadow-none border compose-action text-decoration-none text-dark"> <div v-if="media.length == 0" class="card my-md-3 shadow-none border compose-action text-decoration-none text-dark">
<div @click.prevent="addMedia" class="card-body py-2"> <div @click.prevent="addMedia" class="card-body py-2">
@ -337,7 +341,7 @@
</div> </div>
</div> </div>
<div v-if="page == 'cropPhoto'" class="w-100 h-100"> <div v-else-if="page == 'cropPhoto'" class="w-100 h-100">
<div v-if="ids.length > 0"> <div v-if="ids.length > 0">
<vue-cropper <vue-cropper
ref="cropper" ref="cropper"
@ -352,7 +356,7 @@
</div> </div>
</div> </div>
<div v-if="page == 2" class="w-100 h-100"> <div v-else-if="page == 2" class="w-100 h-100">
<div v-if="media.length == 1"> <div v-if="media.length == 1">
<div slot="img" style="display:flex;min-height: 420px;align-items: center;"> <div slot="img" style="display:flex;min-height: 420px;align-items: center;">
<img :class="'d-block img-fluid w-100 ' + [media[carouselCursor].filter_class?media[carouselCursor].filter_class:'']" :src="media[carouselCursor].url" :alt="media[carouselCursor].description" :title="media[carouselCursor].description"> <img :class="'d-block img-fluid w-100 ' + [media[carouselCursor].filter_class?media[carouselCursor].filter_class:'']" :src="media[carouselCursor].url" :alt="media[carouselCursor].description" :title="media[carouselCursor].description">
@ -368,7 +372,9 @@
</li> </li>
<li class="nav-item" v-for="(filter, index) in filters"> <li class="nav-item" v-for="(filter, index) in filters">
<div class="p-1 pt-3"> <div class="p-1 pt-3">
<img :src="media[carouselCursor].url" width="100px" height="60px" :class="filter[1]" v-on:click.prevent="toggleFilter($event, filter[1])"> <div class="rounded" :class="filter[1]">
<img :src="media[carouselCursor].url" width="100px" height="60px" v-on:click.prevent="toggleFilter($event, filter[1])">
</div>
</div> </div>
<a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-primary active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a> <a :class="[media[carouselCursor].filter_class == filter[1] ? 'nav-link text-primary active' : 'nav-link text-muted']" href="#" v-on:click.prevent="toggleFilter($event, filter[1])">{{filter[0]}}</a>
</li> </li>
@ -376,20 +382,21 @@
</div> </div>
</div> </div>
<div v-else-if="media.length > 1" class="d-flex-inline px-2 pt-2"> <div v-else-if="media.length > 1" class="d-flex-inline px-2 pt-2">
<ul class="nav media-drawer-filters text-center"> <ul class="nav media-drawer-filters text-center pb-3">
<li class="nav-item mx-md-4">&nbsp;</li> <li class="nav-item mx-md-4">&nbsp;</li>
<li v-for="(m, i) in media" class="nav-item mx-md-4"> <li v-for="(m, i) in media" :key="m.id + ':' + carouselCursor" class="nav-item mx-md-4">
<div class="nav-link" style="display:block;width:300px;height:300px;" @click="carouselCursor = i"> <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"> --> <!-- <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:'']"> <div :class="[m.filter_class?m.filter_class:'']" style="width:100%;height:100%;display:block;">
<div :class="'rounded ' + [i == carouselCursor ? ' border border-primary shadow':'']" :style="'display:block;width:100%;height:100%;background-image: url(' + m.url + ');background-size:cover;'"></div>
<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> </div>
</span>
</div> </div>
<div v-if="i == carouselCursor" class="text-center mb-0 small text-lighter font-weight-bold pt-2"> <div v-if="i == carouselCursor" class="text-center mb-0 small text-lighter font-weight-bold pt-2">
<button class="btn btn-link" @click="mediaReorder('prev')"><i class="far fa-chevron-circle-left"></i></button>
<span class="cursor-pointer" @click.prevent="showCropPhotoCard">Crop</span> <span class="cursor-pointer" @click.prevent="showCropPhotoCard">Crop</span>
<span class="cursor-pointer px-3" @click.prevent="showEditMediaCard()">Edit</span> <span class="cursor-pointer px-3" @click.prevent="showEditMediaCard()">Edit</span>
<span class="cursor-pointer" @click="deleteMedia()">Delete</span> <span class="cursor-pointer" @click="deleteMedia()">Delete</span>
<button class="btn btn-link" @click="mediaReorder('next')"><i class="far fa-chevron-circle-right"></i></button>
</div> </div>
</li> </li>
<li class="nav-item mx-md-4">&nbsp;</li> <li class="nav-item mx-md-4">&nbsp;</li>
@ -417,7 +424,7 @@
</div> </div>
</div> </div>
<div v-if="page == 3" class="w-100 h-100"> <div v-else-if="page == 3" class="w-100 h-100">
<div class="border-bottom mt-2"> <div class="border-bottom mt-2">
<div class="media px-3"> <div class="media px-3">
<img :src="media[0].url" width="42px" height="42px" :class="[media[0].filter_class?'mr-2 ' + media[0].filter_class:'mr-2']"> <img :src="media[0].url" width="42px" height="42px" :class="[media[0].filter_class?'mr-2 ' + media[0].filter_class:'mr-2']">
@ -524,7 +531,7 @@
</div> </div>
</div> </div>
<div v-if="page == 'tagPeople'" class="w-100 h-100 p-3"> <div v-else-if="page == 'tagPeople'" class="w-100 h-100 p-3">
<autocomplete <autocomplete
v-show="taggedUsernames.length < 10" v-show="taggedUsernames.length < 10"
:search="tagSearch" :search="tagSearch"
@ -557,14 +564,14 @@
<p class="font-weight-bold text-center small text-muted pt-3 mb-0">When you tag someone, they are sent a notification.<br>For more information on tagging, <a href="#" class="text-primary" @click.prevent="showTagHelpCard()">click here</a>.</p> <p class="font-weight-bold text-center small text-muted pt-3 mb-0">When you tag someone, they are sent a notification.<br>For more information on tagging, <a href="#" class="text-primary" @click.prevent="showTagHelpCard()">click here</a>.</p>
</div> </div>
<div v-if="page == 'tagPeopleHelp'" class="w-100 h-100 p-3"> <div v-else-if="page == 'tagPeopleHelp'" class="w-100 h-100 p-3">
<p class="mb-0 text-center py-3 px-2 lead">Tagging someone is like mentioning them, with the option to make it private between you.</p> <p class="mb-0 text-center py-3 px-2 lead">Tagging someone is like mentioning them, with the option to make it private between you.</p>
<p class="mb-3 py-3 px-2 font-weight-lighter"> <p class="mb-3 py-3 px-2 font-weight-lighter">
You can choose to tag someone in public or private mode. Public mode will allow others to see who you tagged in the post and private mode tagged users will not be shown to others. You can choose to tag someone in public or private mode. Public mode will allow others to see who you tagged in the post and private mode tagged users will not be shown to others.
</p> </p>
</div> </div>
<div v-if="page == 'addLocation'" class="w-100 h-100 p-3"> <div v-else-if="page == 'addLocation'" class="w-100 h-100 p-3">
<p class="mb-0">Add Location</p> <p class="mb-0">Add Location</p>
<autocomplete <autocomplete
:search="locationSearch" :search="locationSearch"
@ -576,7 +583,7 @@
</autocomplete> </autocomplete>
</div> </div>
<div v-if="page == 'advancedSettings'" class="w-100 h-100"> <div v-else-if="page == 'advancedSettings'" class="w-100 h-100">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<!-- <div class="d-none list-group-item d-flex justify-content-between"> <!-- <div class="d-none list-group-item d-flex justify-content-between">
<div> <div>
@ -650,7 +657,7 @@
</div> </div>
</div> </div>
<div v-if="page == 'visibility'" class="w-100 h-100"> <div v-else-if="page == 'visibility'" class="w-100 h-100">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<div <div
v-if="!profile.locked" v-if="!profile.locked"
@ -675,7 +682,7 @@
</div> </div>
</div> </div>
<div v-if="page == 'altText'" class="w-100 h-100 p-3"> <div v-else-if="page == 'altText'" class="w-100 h-100 p-3">
<div v-for="(m, index) in media"> <div v-for="(m, index) in media">
<div class="media"> <div class="media">
<img :src="m.preview_url" class="mr-3" width="50px" height="50px"> <img :src="m.preview_url" class="mr-3" width="50px" height="50px">
@ -692,7 +699,7 @@
</p> </p>
</div> </div>
<div v-if="page == 'addToCollection'" class="w-100 h-100 p-3"> <div v-else-if="page == 'addToCollection'" class="w-100 h-100 p-3">
<div v-if="collectionsLoaded && collections.length" class="list-group mb-3 collections-list-group"> <div v-if="collectionsLoaded && collections.length" class="list-group mb-3 collections-list-group">
<div <div
v-for="(collection, index) in collections" v-for="(collection, index) in collections"
@ -721,19 +728,19 @@
</p> </p>
</div> </div>
<div v-if="page == 'schedulePost'" class="w-100 h-100 p-3"> <div v-else-if="page == 'schedulePost'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p> <p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div> </div>
<div v-if="page == 'mediaMetadata'" class="w-100 h-100 p-3"> <div v-else-if="page == 'mediaMetadata'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p> <p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div> </div>
<div v-if="page == 'addToStory'" class="w-100 h-100 p-3"> <div v-else-if="page == 'addToStory'" class="w-100 h-100 p-3">
<p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p> <p class="text-center lead text-muted mb-0 py-5">This feature is not available yet.</p>
</div> </div>
<div v-if="page == 'editMedia'" class="w-100 h-100 p-3"> <div v-else-if="page == 'editMedia'" class="w-100 h-100 p-3">
<div class="media"> <div class="media">
<img :src="media[carouselCursor].preview_url" class="mr-3" width="50px" height="50px"> <img :src="media[carouselCursor].preview_url" class="mr-3" width="50px" height="50px">
<div class="media-body"> <div class="media-body">
@ -770,7 +777,7 @@
</p> </p>
</div> </div>
<div v-if="page == 'video-2'" class="w-100 h-100"> <div v-else-if="page == 'video-2'" class="w-100 h-100">
<div v-if="video.title.length" class="border-bottom"> <div v-if="video.title.length" class="border-bottom">
<div class="media p-3"> <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']"> <img :src="media[0].url" width="100px" height="70px" :class="[media[0].filter_class?'mr-2 ' + media[0].filter_class:'mr-2']">
@ -833,6 +840,12 @@
</div> </div>
</div> </div>
<div v-else-if="page == 'filteringMedia'" class="w-100 h-100 py-5">
<div class="d-flex flex-column align-items-center justify-content-center py-5">
<b-spinner small />
<p class="font-weight-bold mt-3">Applying filters...</p>
</div>
</div>
</div> </div>
<!-- card-footers --> <!-- card-footers -->
@ -910,6 +923,7 @@ export default {
}, },
namedPages: [ namedPages: [
'filteringMedia',
'cropPhoto', 'cropPhoto',
'tagPeople', 'tagPeople',
'addLocation', 'addLocation',
@ -943,7 +957,6 @@ export default {
cb(res.data); cb(res.data);
}) })
.catch(err => { .catch(err => {
console.log(err);
}) })
}) })
}, },
@ -957,7 +970,6 @@ export default {
cb(res.data); cb(res.data);
}) })
.catch(err => { .catch(err => {
console.log(err);
}) })
}) })
} }
@ -1032,6 +1044,10 @@ export default {
collectionsPage: 1, collectionsPage: 1,
collectionsCanLoadMore: false, collectionsCanLoadMore: false,
spoilerText: undefined, spoilerText: undefined,
isFilteringMedia: false,
filteringMediaTimeout: undefined,
filteringRemainingCount: 0,
isPosting: false,
} }
}, },
@ -1242,6 +1258,50 @@ export default {
}); });
}, },
mediaReorder(dir) {
const m = this.media;
const cur = this.carouselCursor;
const pla = m[cur];
let res = [];
let cursor = 0;
if(dir == 'prev') {
if(cur == 0) {
for (let i = cursor; i < m.length - 1; i++) {
res[i] = m[i+1];
}
res[m.length - 1] = pla;
cursor = 0;
} else {
res = this.handleSwap(m, cur, cur - 1);
cursor = cur - 1;
}
} else {
if(cur == m.length - 1) {
res = m;
let lastItem = res.pop();
res.unshift(lastItem);
cursor = m.length - 1;
} else {
res = this.handleSwap(m, cur, cur + 1);
cursor = cur + 1;
}
}
this.$nextTick(() => {
this.media = res;
this.carouselCursor = cursor;
})
},
handleSwap(arr, index1, index2) {
if (index1 >= 0 && index1 < arr.length && index2 >= 0 && index2 < arr.length) {
const temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
return arr;
}
},
compose() { compose() {
let state = this.composeState; let state = this.composeState;
@ -1254,8 +1314,15 @@ export default {
return; return;
} }
switch(state) { switch(state) {
case 'publish' : case 'publish':
this.isPosting = true;
let count = this.media.filter(m => m.filter_class && !m.hasOwnProperty('is_filtered')).length;
if(count) {
this.applyFilterToMedia();
return;
}
if(this.composeSettings.media_descriptions === true) { if(this.composeSettings.media_descriptions === true) {
let count = this.media.filter(m => { let count = this.media.filter(m => {
return !m.hasOwnProperty('alt') || m.alt.length < 2; return !m.hasOwnProperty('alt') || m.alt.length < 2;
@ -1377,6 +1444,10 @@ export default {
switch(this.mode) { switch(this.mode) {
case 'photo': case 'photo':
switch(this.page) { switch(this.page) {
case 'filteringMedia':
this.page = 2;
break;
case 'addText': case 'addText':
this.page = 1; this.page = 1;
break; break;
@ -1411,6 +1482,10 @@ export default {
case 'video': case 'video':
switch(this.page) { switch(this.page) {
case 'filteringMedia':
this.page = 2;
break;
case 'licensePicker': case 'licensePicker':
this.page = 'video-2'; this.page = 'video-2';
break; break;
@ -1431,6 +1506,10 @@ export default {
this.page = 1; this.page = 1;
break; break;
case 'filteringMedia':
this.page = 2;
break;
case 'textOptions': case 'textOptions':
this.page = 'addText'; this.page = 'addText';
break; break;
@ -1470,6 +1549,9 @@ export default {
this.page = 2; this.page = 2;
break; break;
case 'filteringMedia':
break;
case 'cropPhoto': case 'cropPhoto':
this.pageLoading = true; this.pageLoading = true;
let self = this; let self = this;
@ -1495,14 +1577,7 @@ export default {
break; break;
case 2: case 2:
if(this.currentFilter) {
if(window.confirm('Are you sure you want to apply this filter?')) {
this.applyFilterToMedia();
this.page++;
}
} else {
this.page++; this.page++;
}
break; break;
case 3: case 3:
this.page++; this.page++;
@ -1649,43 +1724,73 @@ export default {
// this is where the magic happens // this is where the magic happens
var ua = navigator.userAgent.toLowerCase(); var ua = navigator.userAgent.toLowerCase();
if(ua.indexOf('firefox') == -1 && ua.indexOf('chrome') == -1) { if(ua.indexOf('firefox') == -1 && ua.indexOf('chrome') == -1) {
this.isPosting = false;
swal('Oops!', 'Your browser does not support the filter feature.', 'error'); swal('Oops!', 'Your browser does not support the filter feature.', 'error');
this.page = 3;
return; return;
} }
let medias = this.media; let count = this.media.filter(m => m.filter_class).length;
let media = null; if(count) {
const canvas = document.getElementById('pr_canvas'); this.page = 'filteringMedia';
const ctx = canvas.getContext('2d'); this.filteringRemainingCount = count;
let image = document.getElementById('pr_img'); this.$nextTick(() => {
let blob = null; this.isFilteringMedia = true;
let data = null; this.media.forEach((media, idx) => this.applyFilterToMediaSave(media, idx));
})
for (var i = medias.length - 1; i >= 0; i--) { } else {
media = medias[i]; this.page = 3;
if(media.filter_class) { }
image.src = media.url;
image.addEventListener('load', e => {
canvas.width = image.width;
canvas.height = image.height;
ctx.filter = App.util.filterCss[media.filter_class];
ctx.drawImage(image, 0, 0, image.width, image.height);
ctx.save();
canvas.toBlob(function(blob) {
data = new FormData();
data.append('file', blob);
data.append('id', media.id);
axios.post('/api/compose/v0/media/update', data).then(res => {
}).catch(err => {
});
});
}, media.mime, 0.9);
ctx.clearRect(0, 0, image.width, image.height);
}
}
}, },
applyFilterToMediaSave(media, idx) {
if(!media.filter_class) {
return;
}
let self = this;
let data = null;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
let image = document.createElement('img');
image.src = media.url;
image.addEventListener('load', e => {
canvas.width = image.width;
canvas.height = image.height;
ctx.filter = App.util.filterCss[media.filter_class];
ctx.drawImage(image, 0, 0, image.width, image.height);
ctx.save();
canvas.toBlob(function(blob) {
data = new FormData();
data.append('file', blob);
data.append('id', media.id);
axios.post('/api/compose/v0/media/update', data)
.then(res => {
self.media[idx].is_filtered = true;
self.updateFilteringMedia();
}).catch(err => {
});
});
}, media.mime, 0.9);
ctx.clearRect(0, 0, image.width, image.height);
},
updateFilteringMedia() {
this.filteringRemainingCount--;
this.filteringMediaTimeout = setTimeout(() => this.filteringMediaTimeoutJob(), 500);
},
filteringMediaTimeoutJob() {
if(this.filteringRemainingCount === 0) {
this.isFilteringMedia = false;
clearTimeout(this.filteringMediaTimeout);
setTimeout(() => this.compose(), 500);
} else {
clearTimeout(this.filteringMediaTimeout);
this.filteringMediaTimeout = setTimeout(() => this.filteringMediaTimeoutJob(), 1000);
}
},
tagSearch(input) { tagSearch(input) {
if (input.length < 1) { return []; } if (input.length < 1) { return []; }
let self = this; let self = this;
@ -1800,7 +1905,6 @@ export default {
} }
window.location.href = res.data.url; window.location.href = res.data.url;
}).catch(err => { }).catch(err => {
console.log(err.response.data.error);
if(err.response.data.hasOwnProperty('error')) { if(err.response.data.hasOwnProperty('error')) {
if(err.response.data.error == 'Duplicate detected.') { if(err.response.data.error == 'Duplicate detected.') {
this.postingPoll = false; this.postingPoll = false;

11
resources/assets/js/util/debounce.js vendored Normal file
View file

@ -0,0 +1,11 @@
export function debounce (fn, delay) {
var timeoutID = null
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}