mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-22 13:03:16 +00:00
Merge branch 'staging' of github.com:mbliznikova/pixelfed into staging
This commit is contained in:
commit
e3de4c3e68
84 changed files with 2957 additions and 886 deletions
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Added
|
||||
- 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 `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 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 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/))
|
||||
|
||||
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
|
||||
|
|
|
@ -10,8 +10,11 @@ use Illuminate\Queue\InteractsWithQueue;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
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;
|
||||
|
||||
|
@ -20,8 +23,34 @@ class MediaDeletePipeline implements ShouldQueue
|
|||
public $timeout = 300;
|
||||
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: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)
|
||||
{
|
||||
$this->media = $media;
|
||||
|
@ -63,9 +92,17 @@ class MediaDeletePipeline implements ShouldQueue
|
|||
$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();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
109
app/Jobs/VideoPipeline/VideoHlsPipeline.php
Normal file
109
app/Jobs/VideoPipeline/VideoHlsPipeline.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -16,13 +16,46 @@ use App\Jobs\MediaPipeline\MediaStoragePipeline;
|
|||
use App\Util\Media\Blurhash;
|
||||
use App\Services\MediaService;
|
||||
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;
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -54,7 +87,7 @@ class VideoThumbnail implements ShouldQueue
|
|||
$path[$i] = $t;
|
||||
$save = implode('/', $path);
|
||||
$video = FFMpeg::open($base)
|
||||
->getFrameFromSeconds(0)
|
||||
->getFrameFromSeconds(1)
|
||||
->export()
|
||||
->toDisk('local')
|
||||
->save($save);
|
||||
|
@ -68,6 +101,9 @@ class VideoThumbnail implements ShouldQueue
|
|||
$media->save();
|
||||
}
|
||||
|
||||
if(config('media.hls.enabled')) {
|
||||
VideoHlsPipeline::dispatch($media)->onQueue('mmo');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
|
||||
}
|
||||
|
|
27
app/Services/Media/MediaHlsService.php
Normal file
27
app/Services/Media/MediaHlsService.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ class MediaService
|
|||
|
||||
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();
|
||||
if(!$media) {
|
||||
return [];
|
||||
|
@ -46,7 +46,8 @@ class MediaService
|
|||
$media['orientation'],
|
||||
$media['filter_name'],
|
||||
$media['filter_class'],
|
||||
$media['mime']
|
||||
$media['mime'],
|
||||
$media['hls_manifest']
|
||||
);
|
||||
|
||||
$media['type'] = $mime ? strtolower($mime[0]) : 'unknown';
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Transformer\Api;
|
|||
|
||||
use App\Media;
|
||||
use League\Fractal;
|
||||
use Storage;
|
||||
|
||||
class MediaTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
|
@ -28,6 +29,10 @@ class MediaTransformer extends Fractal\TransformerAbstract
|
|||
'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) {
|
||||
$res['meta'] = [
|
||||
'focus' => [
|
||||
|
|
|
@ -7,86 +7,100 @@ use Illuminate\Support\Str;
|
|||
|
||||
class Config {
|
||||
|
||||
const CACHE_KEY = 'api:site:configuration:_v0.8';
|
||||
const CACHE_KEY = 'api:site:configuration:_v0.8';
|
||||
|
||||
public static function get() {
|
||||
return Cache::remember(self::CACHE_KEY, 900, function() {
|
||||
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'),
|
||||
public static function get() {
|
||||
return Cache::remember(self::CACHE_KEY, 900, function() {
|
||||
$hls = [
|
||||
'enabled' => config('media.hls.enabled'),
|
||||
];
|
||||
if(config('media.hls.enabled')) {
|
||||
$hls = [
|
||||
'enabled' => true,
|
||||
'debug' => (bool) config('media.hls.debug'),
|
||||
'p2p' => (bool) config('media.hls.p2p'),
|
||||
'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_video' => (bool) config('pixelfed.optimize_video'),
|
||||
'optimize_image' => (bool) config('pixelfed.optimize_image'),
|
||||
'optimize_video' => (bool) config('pixelfed.optimize_video'),
|
||||
|
||||
'media_types' => 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')
|
||||
],
|
||||
'media_types' => 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')
|
||||
],
|
||||
|
||||
'activitypub' => [
|
||||
'enabled' => (bool) config_cache('federation.activitypub.enabled'),
|
||||
'remote_follow' => config('federation.activitypub.remoteFollow')
|
||||
],
|
||||
'activitypub' => [
|
||||
'enabled' => (bool) config_cache('federation.activitypub.enabled'),
|
||||
'remote_follow' => config('federation.activitypub.remoteFollow')
|
||||
],
|
||||
|
||||
'ab' => config('exp'),
|
||||
'ab' => config('exp'),
|
||||
|
||||
'site' => [
|
||||
'name' => config_cache('app.name'),
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'url' => config('app.url'),
|
||||
'description' => config_cache('app.short_description')
|
||||
],
|
||||
'site' => [
|
||||
'name' => config_cache('app.name'),
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'url' => config('app.url'),
|
||||
'description' => config_cache('app.short_description')
|
||||
],
|
||||
|
||||
'account' => [
|
||||
'max_avatar_size' => config('pixelfed.max_avatar_size'),
|
||||
'max_bio_length' => config('pixelfed.max_bio_length'),
|
||||
'max_name_length' => config('pixelfed.max_name_length'),
|
||||
'min_password_length' => config('pixelfed.min_password_length'),
|
||||
'max_account_size' => config('pixelfed.max_account_size')
|
||||
],
|
||||
'account' => [
|
||||
'max_avatar_size' => config('pixelfed.max_avatar_size'),
|
||||
'max_bio_length' => config('pixelfed.max_bio_length'),
|
||||
'max_name_length' => config('pixelfed.max_name_length'),
|
||||
'min_password_length' => config('pixelfed.min_password_length'),
|
||||
'max_account_size' => config('pixelfed.max_account_size')
|
||||
],
|
||||
|
||||
'username' => [
|
||||
'remote' => [
|
||||
'formats' => config('instance.username.remote.formats'),
|
||||
'format' => config('instance.username.remote.format'),
|
||||
'custom' => config('instance.username.remote.custom')
|
||||
]
|
||||
],
|
||||
'username' => [
|
||||
'remote' => [
|
||||
'formats' => config('instance.username.remote.formats'),
|
||||
'format' => config('instance.username.remote.format'),
|
||||
'custom' => config('instance.username.remote.custom')
|
||||
]
|
||||
],
|
||||
|
||||
'features' => [
|
||||
'timelines' => [
|
||||
'local' => true,
|
||||
'network' => (bool) config('federation.network_timeline'),
|
||||
],
|
||||
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
|
||||
'stories' => (bool) config_cache('instance.stories.enabled'),
|
||||
'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
|
||||
'import' => [
|
||||
'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'),
|
||||
'mastodon' => false,
|
||||
'pixelfed' => false
|
||||
],
|
||||
'label' => [
|
||||
'covid' => [
|
||||
'enabled' => (bool) config('instance.label.covid.enabled'),
|
||||
'org' => config('instance.label.covid.org'),
|
||||
'url' => config('instance.label.covid.url'),
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
'features' => [
|
||||
'timelines' => [
|
||||
'local' => true,
|
||||
'network' => (bool) config('federation.network_timeline'),
|
||||
],
|
||||
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
|
||||
'stories' => (bool) config_cache('instance.stories.enabled'),
|
||||
'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
|
||||
'import' => [
|
||||
'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'),
|
||||
'mastodon' => false,
|
||||
'pixelfed' => false
|
||||
],
|
||||
'label' => [
|
||||
'covid' => [
|
||||
'enabled' => (bool) config('instance.label.covid.enabled'),
|
||||
'org' => config('instance.label.covid.org'),
|
||||
'url' => config('instance.label.covid.url'),
|
||||
]
|
||||
],
|
||||
'hls' => $hls
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public static function json() {
|
||||
return json_encode(self::get(), JSON_FORCE_OBJECT);
|
||||
}
|
||||
public static function json() {
|
||||
return json_encode(self::get(), JSON_FORCE_OBJECT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
return [
|
||||
'ffmpeg' => [
|
||||
'binaries' => env('FFMPEG_BINARIES', 'ffmpeg'),
|
||||
|
||||
'threads' => 12, // set to false to disable the default 'threads' filter
|
||||
'threads' => env('FFMPEG_THREADS', false),
|
||||
],
|
||||
|
||||
'ffprobe' => [
|
||||
|
@ -18,4 +17,6 @@ return [
|
|||
'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())),
|
||||
|
||||
'min_hls_version' => env('FFMPEG_MIN_HLS_VERSION', '4.3.0'),
|
||||
];
|
||||
|
|
|
@ -22,5 +22,39 @@ return [
|
|||
|
||||
'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
2081
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -34,9 +34,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"bigpicture": "^2.6.2",
|
||||
"blurhash": "^1.1.3",
|
||||
|
|
BIN
public/js/account-import.js
vendored
BIN
public/js/account-import.js
vendored
Binary file not shown.
BIN
public/js/activity.js
vendored
BIN
public/js/activity.js
vendored
Binary file not shown.
BIN
public/js/admin.js
vendored
BIN
public/js/admin.js
vendored
Binary file not shown.
BIN
public/js/admin_invite.js
vendored
BIN
public/js/admin_invite.js
vendored
Binary file not shown.
BIN
public/js/changelog.bundle.742a06ba0a547120.js
vendored
Normal file
BIN
public/js/changelog.bundle.742a06ba0a547120.js
vendored
Normal file
Binary file not shown.
BIN
public/js/changelog.bundle.c4c82057f9628c72.js
vendored
BIN
public/js/changelog.bundle.c4c82057f9628c72.js
vendored
Binary file not shown.
BIN
public/js/collectioncompose.js
vendored
BIN
public/js/collectioncompose.js
vendored
Binary file not shown.
BIN
public/js/collections.js
vendored
BIN
public/js/collections.js
vendored
Binary file not shown.
BIN
public/js/compose-classic.js
vendored
BIN
public/js/compose-classic.js
vendored
Binary file not shown.
BIN
public/js/compose.chunk.6464688bf5b5ef97.js
vendored
BIN
public/js/compose.chunk.6464688bf5b5ef97.js
vendored
Binary file not shown.
BIN
public/js/compose.chunk.965eab35620423e5.js
vendored
Normal file
BIN
public/js/compose.chunk.965eab35620423e5.js
vendored
Normal file
Binary file not shown.
BIN
public/js/compose.js
vendored
BIN
public/js/compose.js
vendored
Binary file not shown.
BIN
public/js/daci.chunk.b17a0b11877389d7.js
vendored
Normal file
BIN
public/js/daci.chunk.b17a0b11877389d7.js
vendored
Normal file
Binary file not shown.
BIN
public/js/daci.chunk.bfa9e4f459fec835.js
vendored
BIN
public/js/daci.chunk.bfa9e4f459fec835.js
vendored
Binary file not shown.
BIN
public/js/developers.js
vendored
BIN
public/js/developers.js
vendored
Binary file not shown.
BIN
public/js/direct.js
vendored
BIN
public/js/direct.js
vendored
Binary file not shown.
BIN
public/js/discover.chunk.56d2d8cfbbecc761.js
vendored
BIN
public/js/discover.chunk.56d2d8cfbbecc761.js
vendored
Binary file not shown.
BIN
public/js/discover.chunk.9606885dad3c8a99.js
vendored
Normal file
BIN
public/js/discover.chunk.9606885dad3c8a99.js
vendored
Normal file
Binary file not shown.
BIN
public/js/discover.js
vendored
BIN
public/js/discover.js
vendored
Binary file not shown.
BIN
public/js/discover~findfriends.chunk.02be60ab26503531.js
vendored
Normal file
BIN
public/js/discover~findfriends.chunk.02be60ab26503531.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~hashtag.bundle.9cfffc517f35044e.js
vendored
Normal file
BIN
public/js/discover~hashtag.bundle.9cfffc517f35044e.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~memories.chunk.ce9cc6446020e9b3.js
vendored
Normal file
BIN
public/js/discover~memories.chunk.ce9cc6446020e9b3.js
vendored
Normal file
Binary file not shown.
BIN
public/js/discover~myhashtags.chunk.6eab2414b2b16e19.js
vendored
Normal file
BIN
public/js/discover~myhashtags.chunk.6eab2414b2b16e19.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~serverfeed.chunk.0f2dcc473fdce17e.js
vendored
Normal file
BIN
public/js/discover~serverfeed.chunk.0f2dcc473fdce17e.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~settings.chunk.732c1f76a00d9204.js
vendored
Normal file
BIN
public/js/discover~settings.chunk.732c1f76a00d9204.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/js/dms~message.chunk.15157ff4a6c17cc7.js
vendored
Normal file
BIN
public/js/dms~message.chunk.15157ff4a6c17cc7.js
vendored
Normal file
Binary file not shown.
BIN
public/js/dms~message.chunk.990c68dfc266b0cf.js
vendored
BIN
public/js/dms~message.chunk.990c68dfc266b0cf.js
vendored
Binary file not shown.
BIN
public/js/error404.bundle.182d0aaa2da9ed23.js
vendored
BIN
public/js/error404.bundle.182d0aaa2da9ed23.js
vendored
Binary file not shown.
BIN
public/js/error404.bundle.3bbc118159460db6.js
vendored
Normal file
BIN
public/js/error404.bundle.3bbc118159460db6.js
vendored
Normal file
Binary file not shown.
BIN
public/js/hashtag.js
vendored
BIN
public/js/hashtag.js
vendored
Binary file not shown.
BIN
public/js/home.chunk.351f55e9d09b6482.js
vendored
Normal file
BIN
public/js/home.chunk.351f55e9d09b6482.js
vendored
Normal file
Binary file not shown.
BIN
public/js/home.chunk.bd623a430a5584c2.js
vendored
BIN
public/js/home.chunk.bd623a430a5584c2.js
vendored
Binary file not shown.
BIN
public/js/i18n.bundle.47cbf9f04d955267.js
vendored
Normal file
BIN
public/js/i18n.bundle.47cbf9f04d955267.js
vendored
Normal file
Binary file not shown.
BIN
public/js/i18n.bundle.4a5ff18de549ac4e.js
vendored
BIN
public/js/i18n.bundle.4a5ff18de549ac4e.js
vendored
Binary file not shown.
BIN
public/js/landing.js
vendored
BIN
public/js/landing.js
vendored
Binary file not shown.
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
Binary file not shown.
BIN
public/js/portfolio.js
vendored
BIN
public/js/portfolio.js
vendored
Binary file not shown.
BIN
public/js/post.chunk.729ca668f46545cb.js
vendored
BIN
public/js/post.chunk.729ca668f46545cb.js
vendored
Binary file not shown.
BIN
public/js/post.chunk.74f8b1d1954f5d01.js
vendored
Normal file
BIN
public/js/post.chunk.74f8b1d1954f5d01.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile-directory.js
vendored
BIN
public/js/profile-directory.js
vendored
Binary file not shown.
BIN
public/js/profile.chunk.029572d9018fc65f.js
vendored
BIN
public/js/profile.chunk.029572d9018fc65f.js
vendored
Binary file not shown.
BIN
public/js/profile.chunk.0e5bd852054d6355.js
vendored
Normal file
BIN
public/js/profile.chunk.0e5bd852054d6355.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.js
vendored
BIN
public/js/profile.js
vendored
Binary file not shown.
BIN
public/js/profile~followers.bundle.731f680cfb96563d.js
vendored
Normal file
BIN
public/js/profile~followers.bundle.731f680cfb96563d.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/profile~following.bundle.3d95796c9f1678dd.js
vendored
Normal file
BIN
public/js/profile~following.bundle.3d95796c9f1678dd.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/remote_auth.js
vendored
BIN
public/js/remote_auth.js
vendored
Binary file not shown.
BIN
public/js/search.js
vendored
BIN
public/js/search.js
vendored
Binary file not shown.
BIN
public/js/spa.js
vendored
BIN
public/js/spa.js
vendored
Binary file not shown.
BIN
public/js/status.js
vendored
BIN
public/js/status.js
vendored
Binary file not shown.
BIN
public/js/stories.js
vendored
BIN
public/js/stories.js
vendored
Binary file not shown.
BIN
public/js/story-compose.js
vendored
BIN
public/js/story-compose.js
vendored
Binary file not shown.
BIN
public/js/timeline.js
vendored
BIN
public/js/timeline.js
vendored
Binary file not shown.
BIN
public/js/vendor.js
vendored
BIN
public/js/vendor.js
vendored
Binary file not shown.
|
@ -31,13 +31,13 @@
|
|||
*/
|
||||
|
||||
/*!
|
||||
* Cropper.js v1.5.13
|
||||
* Cropper.js v1.6.1
|
||||
* https://fengyuanchen.github.io/cropperjs
|
||||
*
|
||||
* Copyright 2015-present Chen Fengyuan
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* 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
|
||||
* (c) 2014-2022 Evan You
|
||||
|
@ -63,14 +77,14 @@
|
|||
*/
|
||||
|
||||
/*!
|
||||
* jQuery JavaScript Library v3.7.0
|
||||
* jQuery JavaScript Library v3.7.1
|
||||
* https://jquery.com/
|
||||
*
|
||||
* Copyright OpenJS Foundation and other contributors
|
||||
* Released under the MIT 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 */
|
||||
|
||||
/*! 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
|
||||
* (c) 2015-present Evan You
|
||||
|
@ -158,6 +184,23 @@ and limitations under the License.
|
|||
* 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
|
||||
*
|
||||
|
|
Binary file not shown.
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
|
||||
<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 v-else-if="status.pf_type === 'photo:album'" class="w-100">
|
||||
|
@ -108,27 +108,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="status.pf_type === 'video'">
|
||||
<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>
|
||||
<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>
|
||||
<video-player
|
||||
v-else-if="status.pf_type === 'video'"
|
||||
:status="status"
|
||||
:fixedHeight="fixedHeight"
|
||||
/>
|
||||
|
||||
<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 }"/>
|
||||
|
@ -185,12 +169,14 @@
|
|||
<script type="text/javascript">
|
||||
import BigPicture from 'bigpicture';
|
||||
import ReadMore from './ReadMore.vue';
|
||||
import VideoPlayer from './../../presenter/VideoPlayer.vue';
|
||||
|
||||
export default {
|
||||
props: ['status'],
|
||||
|
||||
components: {
|
||||
"read-more": ReadMore,
|
||||
"video-player": VideoPlayer
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
198
resources/assets/components/presenter/VideoPlayer.vue
Normal file
198
resources/assets/components/presenter/VideoPlayer.vue
Normal 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>
|
|
@ -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">@{{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>
|
|
@ -178,9 +178,13 @@
|
|||
</span>
|
||||
<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 == 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 == '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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -201,10 +205,10 @@
|
|||
</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 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="media px-3">
|
||||
<div class="media-body">
|
||||
|
@ -236,7 +240,7 @@
|
|||
</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 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">
|
||||
|
@ -337,7 +341,7 @@
|
|||
</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">
|
||||
<vue-cropper
|
||||
ref="cropper"
|
||||
|
@ -352,7 +356,7 @@
|
|||
</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 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">
|
||||
|
@ -368,7 +372,9 @@
|
|||
</li>
|
||||
<li class="nav-item" v-for="(filter, index) in filters">
|
||||
<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>
|
||||
<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>
|
||||
|
@ -376,20 +382,21 @@
|
|||
</div>
|
||||
</div>
|
||||
<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"> </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">
|
||||
<!-- <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 :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>
|
||||
</div>
|
||||
</div>
|
||||
<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 px-3" @click.prevent="showEditMediaCard()">Edit</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>
|
||||
</li>
|
||||
<li class="nav-item mx-md-4"> </li>
|
||||
|
@ -417,7 +424,7 @@
|
|||
</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="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']">
|
||||
|
@ -524,7 +531,7 @@
|
|||
</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
|
||||
v-show="taggedUsernames.length < 10"
|
||||
: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>
|
||||
</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-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.
|
||||
</p>
|
||||
</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>
|
||||
<autocomplete
|
||||
:search="locationSearch"
|
||||
|
@ -576,7 +583,7 @@
|
|||
</autocomplete>
|
||||
</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="d-none list-group-item d-flex justify-content-between">
|
||||
<div>
|
||||
|
@ -650,7 +657,7 @@
|
|||
</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
|
||||
v-if="!profile.locked"
|
||||
|
@ -675,7 +682,7 @@
|
|||
</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 class="media">
|
||||
<img :src="m.preview_url" class="mr-3" width="50px" height="50px">
|
||||
|
@ -692,7 +699,7 @@
|
|||
</p>
|
||||
</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-for="(collection, index) in collections"
|
||||
|
@ -721,19 +728,19 @@
|
|||
</p>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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">
|
||||
<img :src="media[carouselCursor].preview_url" class="mr-3" width="50px" height="50px">
|
||||
<div class="media-body">
|
||||
|
@ -770,7 +777,7 @@
|
|||
</p>
|
||||
</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 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']">
|
||||
|
@ -833,6 +840,12 @@
|
|||
</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>
|
||||
|
||||
<!-- card-footers -->
|
||||
|
@ -910,6 +923,7 @@ export default {
|
|||
},
|
||||
|
||||
namedPages: [
|
||||
'filteringMedia',
|
||||
'cropPhoto',
|
||||
'tagPeople',
|
||||
'addLocation',
|
||||
|
@ -943,7 +957,6 @@ export default {
|
|||
cb(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@ -957,7 +970,6 @@ export default {
|
|||
cb(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1032,6 +1044,10 @@ export default {
|
|||
collectionsPage: 1,
|
||||
collectionsCanLoadMore: false,
|
||||
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() {
|
||||
let state = this.composeState;
|
||||
|
||||
|
@ -1254,8 +1314,15 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
let count = this.media.filter(m => {
|
||||
return !m.hasOwnProperty('alt') || m.alt.length < 2;
|
||||
|
@ -1377,6 +1444,10 @@ export default {
|
|||
switch(this.mode) {
|
||||
case 'photo':
|
||||
switch(this.page) {
|
||||
case 'filteringMedia':
|
||||
this.page = 2;
|
||||
break;
|
||||
|
||||
case 'addText':
|
||||
this.page = 1;
|
||||
break;
|
||||
|
@ -1411,6 +1482,10 @@ export default {
|
|||
|
||||
case 'video':
|
||||
switch(this.page) {
|
||||
case 'filteringMedia':
|
||||
this.page = 2;
|
||||
break;
|
||||
|
||||
case 'licensePicker':
|
||||
this.page = 'video-2';
|
||||
break;
|
||||
|
@ -1431,6 +1506,10 @@ export default {
|
|||
this.page = 1;
|
||||
break;
|
||||
|
||||
case 'filteringMedia':
|
||||
this.page = 2;
|
||||
break;
|
||||
|
||||
case 'textOptions':
|
||||
this.page = 'addText';
|
||||
break;
|
||||
|
@ -1470,6 +1549,9 @@ export default {
|
|||
this.page = 2;
|
||||
break;
|
||||
|
||||
case 'filteringMedia':
|
||||
break;
|
||||
|
||||
case 'cropPhoto':
|
||||
this.pageLoading = true;
|
||||
let self = this;
|
||||
|
@ -1495,14 +1577,7 @@ export default {
|
|||
break;
|
||||
|
||||
case 2:
|
||||
if(this.currentFilter) {
|
||||
if(window.confirm('Are you sure you want to apply this filter?')) {
|
||||
this.applyFilterToMedia();
|
||||
this.page++;
|
||||
}
|
||||
} else {
|
||||
this.page++;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
this.page++;
|
||||
|
@ -1649,43 +1724,73 @@ export default {
|
|||
// this is where the magic happens
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
if(ua.indexOf('firefox') == -1 && ua.indexOf('chrome') == -1) {
|
||||
this.isPosting = false;
|
||||
swal('Oops!', 'Your browser does not support the filter feature.', 'error');
|
||||
this.page = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
let medias = this.media;
|
||||
let media = null;
|
||||
const canvas = document.getElementById('pr_canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let image = document.getElementById('pr_img');
|
||||
let blob = null;
|
||||
let data = null;
|
||||
|
||||
for (var i = medias.length - 1; i >= 0; i--) {
|
||||
media = medias[i];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let count = this.media.filter(m => m.filter_class).length;
|
||||
if(count) {
|
||||
this.page = 'filteringMedia';
|
||||
this.filteringRemainingCount = count;
|
||||
this.$nextTick(() => {
|
||||
this.isFilteringMedia = true;
|
||||
this.media.forEach((media, idx) => this.applyFilterToMediaSave(media, idx));
|
||||
})
|
||||
} else {
|
||||
this.page = 3;
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
if (input.length < 1) { return []; }
|
||||
let self = this;
|
||||
|
@ -1800,7 +1905,6 @@ export default {
|
|||
}
|
||||
window.location.href = res.data.url;
|
||||
}).catch(err => {
|
||||
console.log(err.response.data.error);
|
||||
if(err.response.data.hasOwnProperty('error')) {
|
||||
if(err.response.data.error == 'Duplicate detected.') {
|
||||
this.postingPoll = false;
|
||||
|
|
11
resources/assets/js/util/debounce.js
vendored
Normal file
11
resources/assets/js/util/debounce.js
vendored
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue