1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/pixelfed_ynh.git synced 2024-09-03 20:06:04 +02:00

Merge pull request #1511 from pixelfed/frontend-ui-refactor

Add Collections Feature
This commit is contained in:
daniel 2019-07-17 21:23:20 -06:00 committed by GitHub
commit b9697d15d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 3607 additions and 423 deletions

View file

@ -2,6 +2,7 @@
namespace App;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Pixelfed\Snowflake\HasSnowflakePrimary;
@ -16,8 +17,34 @@ class Collection extends Model
*/
public $incrementing = false;
public $fillable = ['profile_id', 'published_at'];
public $dates = ['published_at'];
public function profile()
{
return $this->belongsTo(Profile::class);
}
public function items()
{
return $this->hasMany(CollectionItem::class);
}
public function posts()
{
return $this->hasManyThrough(
Status::class,
CollectionItem::class,
'collection_id',
'id',
'id',
'object_id',
);
}
public function url()
{
return url("/c/{$this->id}");
}
}

View file

@ -9,6 +9,13 @@ class CollectionItem extends Model
{
use HasSnowflakePrimary;
public $fillable = [
'collection_id',
'object_type',
'object_id',
'order'
];
/**
* Indicates if the IDs are auto-incrementing.
*

View file

@ -3,8 +3,199 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\{
Collection,
CollectionItem,
Profile,
Status
};
use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
StatusTransformer,
};
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
class CollectionController extends Controller
{
//
public function create(Request $request)
{
abort_if(!Auth::check(), 403);
$profile = Auth::user()->profile;
$collection = Collection::firstOrCreate([
'profile_id' => $profile->id,
'published_at' => null
]);
return view('collection.create', compact('collection'));
}
public function show(Request $request, int $collection)
{
$collection = Collection::whereNotNull('published_at')->findOrFail($collection);
if($collection->profile->status != null) {
abort(404);
}
if($collection->visibility !== 'public') {
abort_if(!Auth::check() || Auth::user()->profile_id != $collection->profile_id, 404);
}
return view('collection.show', compact('collection'));
}
public function index(Request $request)
{
abort_if(!Auth::check(), 403);
return $request->all();
}
public function store(Request $request, int $id)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'title' => 'nullable',
'description' => 'nullable',
'visibility' => 'required|alpha|in:public,private'
]);
$profile = Auth::user()->profile;
$collection = Collection::whereProfileId($profile->id)->findOrFail($id);
$collection->title = e($request->input('title'));
$collection->description = e($request->input('description'));
$collection->visibility = e($request->input('visibility'));
$collection->save();
return 200;
}
public function publish(Request $request, int $id)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'title' => 'nullable',
'description' => 'nullable',
'visibility' => 'required|alpha|in:public,private'
]);
$profile = Auth::user()->profile;
$collection = Collection::whereProfileId($profile->id)->findOrFail($id);
$collection->title = e($request->input('title'));
$collection->description = e($request->input('description'));
$collection->visibility = e($request->input('visibility'));
$collection->published_at = now();
$collection->save();
return $collection->url();
}
public function delete(Request $request, int $id)
{
abort_if(!Auth::check(), 403);
$user = Auth::user();
$collection = Collection::whereProfileId($user->profile_id)->findOrFail($id);
$collection->items()->delete();
$collection->delete();
return 200;
}
public function storeId(Request $request)
{
$this->validate($request, [
'collection_id' => 'required|int|min:1|exists:collections,id',
'post_id' => 'required|int|min:1|exists:statuses,id'
]);
$profileId = Auth::user()->profile_id;
$collectionId = $request->input('collection_id');
$postId = $request->input('post_id');
$collection = Collection::whereProfileId($profileId)->findOrFail($collectionId);
$count = $collection->items()->count();
if($count >= 18) {
abort(400, 'You can only add 18 posts per collection');
}
$status = Status::whereScope('public')
->whereIn('type', ['photo'])
->findOrFail($postId);
$item = CollectionItem::firstOrCreate([
'collection_id' => $collection->id,
'object_type' => 'App\Status',
'object_id' => $status->id
],[
'order' => $count,
]);
return 200;
}
public function get(Request $request, int $id)
{
$profile = Auth::check() ? Auth::user()->profile : [];
$collection = Collection::whereVisibility('public')->findOrFail($id);
if($collection->published_at == null) {
if(!Auth::check() || $profile->id !== $collection->profile_id) {
abort(404);
}
}
return [
'id' => $collection->id,
'title' => $collection->title,
'description' => $collection->description,
'visibility' => $collection->visibility
];
}
public function getItems(Request $request, int $id)
{
$collection = Collection::findOrFail($id);
if($collection->visibility !== 'public') {
abort_if(!Auth::check() || Auth::user()->profile_id != $collection->profile_id, 404);
}
$posts = $collection->posts()->orderBy('order', 'asc')->paginate(18);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($posts, new StatusTransformer());
$res = $fractal->createData($resource)->toArray();
return response()->json($res);
}
public function getUserCollections(Request $request, int $id)
{
$profile = Profile::whereNull('status')
->whereNull('domain')
->findOrFail($id);
if($profile->is_private) {
abort_if(!Auth::check(), 404);
abort_if(!$profile->followedBy(Auth::user()->profile) && $profile->id != Auth::user()->profile_id, 404);
}
return $profile
->collections()
->has('posts')
->with('posts')
->whereVisibility('public')
->whereNotNull('published_at')
->orderByDesc('published_at')
->paginate(9)
->map(function($collection) {
return [
'id' => $collection->id,
'title' => $collection->title,
'description' => $collection->description,
'thumb' => $collection->posts()->first()->thumb(),
'url' => $collection->url(),
'published_at' => $collection->published_at
];
});
}
}

View file

@ -291,4 +291,9 @@ class Profile extends Model
{
return $this->hasMany(HashtagFollow::class);
}
public function collections()
{
return $this->hasMany(Collection::class);
}
}

View file

@ -68,4 +68,19 @@ trait User {
{
return 100;
}
public function getMaxCollectionsPerHourAttribute()
{
return 10;
}
public function getMaxCollectionsPerDayAttribute()
{
return 20;
}
public function getMaxCollectionsPerMonthAttribute()
{
return 100;
}
}

View file

@ -42,6 +42,7 @@
"fzaninotto/faker": "^1.4",
"mockery/mockery": "^1.0",
"nunomaduro/collision": "^2.0",
"nunomaduro/phpinsights": "^1.7",
"phpunit/phpunit": "^7.5"
},
"autoload": {

3190
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -150,7 +150,6 @@ return [
/*
* Package Service Providers...
*/
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
/*
* Application Service Providers...
@ -211,7 +210,6 @@ return [
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'DotenvEditor' => Jackiedo\DotenvEditor\Facades\DotenvEditor::class,
'PrettyNumber' => App\Util\Lexer\PrettyNumber::class,
'Purify' => Stevebauman\Purify\Facades\Purify::class,
'FFMpeg' => Pbmedia\LaravelFFMpeg\FFMpegFacade::class,

View file

@ -3,7 +3,7 @@
return [
'announcement' => [
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', true),
'enabled' => env('INSTANCE_ANNOUNCEMENT_ENABLED', false),
'message' => env('INSTANCE_ANNOUNCEMENT_MESSAGE', 'Example announcement message.<br><span class="font-weight-normal">Something else here</span>')
],

View file

@ -23,7 +23,7 @@ return [
| This value is the version of your Pixelfed instance.
|
*/
'version' => '0.9.6',
'version' => '0.10.0',
/*
|--------------------------------------------------------------------------

View file

@ -1,4 +1,4 @@
FROM php:7.3-apache
FROM php:7.3-apache-buster
ARG COMPOSER_VERSION="1.8.5"
ARG COMPOSER_CHECKSUM="4e4c1cd74b54a26618699f3190e6f5fc63bb308b13fa660f71f2a2df047c0e17"
@ -7,13 +7,13 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends apt-utils \
&& apt-get install -y --no-install-recommends git gosu \
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev libcurl4-openssl-dev \
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-3 \
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-6 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev \
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
&& locale-gen && update-locale \
&& docker-php-source extract \
&& docker-php-ext-configure gd \
--with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
--enable-freetype \
--with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
--with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
--with-webp-dir=/usr/lib/x86_64-linux-gnu/ \

View file

@ -1,24 +1,25 @@
FROM php:7.2-fpm
FROM php:7.3-fpm-buster
ARG COMPOSER_VERSION="1.8.5"
ARG COMPOSER_CHECKSUM="4e4c1cd74b54a26618699f3190e6f5fc63bb308b13fa660f71f2a2df047c0e17"
RUN apt-get update \
&& apt-get install -y --no-install-recommends apt-utils \
&& apt-get install -y --no-install-recommends git gosu \
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev \
libfreetype6 libjpeg62-turbo libpng16-16 libxpm4 libvpx4 libmagickwand-6.q16-3 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libvpx-dev libmagickwand-dev \
optipng pngquant jpegoptim gifsicle libpq-dev libsqlite3-dev locales zip unzip libzip-dev libcurl4-openssl-dev \
libfreetype6 libicu-dev libjpeg62-turbo libpng16-16 libxpm4 libwebp6 libmagickwand-6.q16-6 \
libfreetype6-dev libjpeg62-turbo-dev libpng-dev libxpm-dev libwebp-dev libmagickwand-dev \
&& sed -i '/en_US/s/^#//g' /etc/locale.gen \
&& locale-gen && update-locale \
&& docker-php-source extract \
&& docker-php-ext-configure gd \
--with-freetype-dir=/usr/lib/x86_64-linux-gnu/ \
--enable-freetype \
--with-jpeg-dir=/usr/lib/x86_64-linux-gnu/ \
--with-xpm-dir=/usr/lib/x86_64-linux-gnu/ \
--with-vpx-dir=/usr/lib/x86_64-linux-gnu/ \
&& docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite pcntl gd exif bcmath intl zip \
--with-webp-dir=/usr/lib/x86_64-linux-gnu/ \
&& docker-php-ext-install pdo_mysql pdo_pgsql pdo_sqlite pcntl gd exif bcmath intl zip curl \
&& pecl install imagick \
&& docker-php-ext-enable imagick pcntl imagick gd exif zip \
&& docker-php-ext-enable imagick pcntl imagick gd exif zip curl \
&& curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /usr/bin/composer \
&& echo "${COMPOSER_CHECKSUM} /usr/bin/composer" | sha256sum -c - \
&& chmod 755 /usr/bin/composer \
@ -32,7 +33,7 @@ ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
COPY . /var/www/
WORKDIR /var/www/
RUN cp -r storage storage.skel \
RUN mkdir public.ext && cp -r storage storage.skel \
&& cp contrib/docker/php.ini /usr/local/etc/php/conf.d/pixelfed.ini \
&& composer install --prefer-dist --no-interaction \
&& rm -rf html && ln -s public html

File diff suppressed because one or more lines are too long

1
public/js/collections.js vendored Normal file
View file

@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[6],{17:function(t,e,s){t.exports=s("ntcu")},BzCV:function(t,e,s){"use strict";s.r(e);var n={props:["collection-id"],data:function(){return{loaded:!1,posts:[]}},beforeMount:function(){this.fetchItems()},mounted:function(){},methods:{fetchItems:function(){var t=this;axios.get("/api/local/collection/items/"+this.collectionId).then(function(e){t.posts=e.data})},previewUrl:function(t){return t.sensitive?"/storage/no-preview.png?v="+(new Date).getTime():t.media_attachments[0].preview_url},previewBackground:function(t){return"background-image: url("+this.previewUrl(t)+");"}}},a=s("KHd+"),i=Object(a.a)(n,function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",[s("div",{staticClass:"row"},t._l(t.posts,function(e,n){return s("div",{staticClass:"col-4 p-0 p-sm-2 p-md-3 p-xs-1"},[s("a",{staticClass:"card info-overlay card-md-border-0",attrs:{href:e.url}},[s("div",{staticClass:"square"},["photo:album"==e.pf_type?s("span",{staticClass:"float-right mr-3 post-icon"},[s("i",{staticClass:"fas fa-images fa-2x"})]):t._e(),t._v(" "),"video"==e.pf_type?s("span",{staticClass:"float-right mr-3 post-icon"},[s("i",{staticClass:"fas fa-video fa-2x"})]):t._e(),t._v(" "),"video:album"==e.pf_type?s("span",{staticClass:"float-right mr-3 post-icon"},[s("i",{staticClass:"fas fa-film fa-2x"})]):t._e(),t._v(" "),s("div",{staticClass:"square-content",style:t.previewBackground(e)}),t._v(" "),s("div",{staticClass:"info-overlay-text"},[s("h5",{staticClass:"text-white m-auto font-weight-bold"},[s("span",[s("span",{staticClass:"far fa-heart fa-lg p-2 d-flex-inline"}),t._v(" "),s("span",{staticClass:"d-flex-inline"},[t._v(t._s(e.favourites_count))])]),t._v(" "),s("span",[s("span",{staticClass:"fas fa-retweet fa-lg p-2 d-flex-inline"}),t._v(" "),s("span",{staticClass:"d-flex-inline"},[t._v(t._s(e.reblogs_count))])])])])])])])}),0)])},[],!1,null,"5edf1095",null);e.default=i.exports},"KHd+":function(t,e,s){"use strict";function n(t,e,s,n,a,i,o,r){var c,l="function"==typeof t?t.options:t;if(e&&(l.render=e,l.staticRenderFns=s,l._compiled=!0),n&&(l.functional=!0),i&&(l._scopeId="data-v-"+i),o?(c=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),a&&a.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(o)},l._ssrRegister=c):a&&(c=r?function(){a.call(this,this.$root.$options.shadowRoot)}:a),c)if(l.functional){l._injectStyles=c;var f=l.render;l.render=function(t,e){return c.call(e),f(t,e)}}else{var p=l.beforeCreate;l.beforeCreate=p?[].concat(p,c):[c]}return{exports:t,options:l}}s.d(e,"a",function(){return n})},ntcu:function(t,e,s){Vue.component("collection-component",s("BzCV").default)}},[[17,0]]]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/loops.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/quill.js vendored

File diff suppressed because one or more lines are too long

2
public/js/search.js vendored

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[17],{14:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)}),ace.require(["ace/theme/monokai"],function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)})}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[14,0]]]);
(window.webpackJsonp=window.webpackJsonp||[]).push([[18],{14:function(e,a,o){e.exports=o("YMO/")},"YMO/":function(e,a,o){(function(e){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,a,o){a.isDark=!0,a.cssClass="ace-monokai",a.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}",e("../lib/dom").importCssString(a.cssText,a.cssClass)}),ace.require(["ace/theme/monokai"],function(c){"object"==o(e)&&"object"==o(a)&&e&&(e.exports=c)})}).call(this,o("YuTi")(e))},YuTi:function(e,a){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}}},[[14,0]]]);

File diff suppressed because one or more lines are too long

2
public/js/vendor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{
"/js/manifest.js": "/js/manifest.js?id=01c8731923a46c30aaed",
"/js/vendor.js": "/js/vendor.js?id=6e7489157aac9e82dbbe",
"/js/vendor.js": "/js/vendor.js?id=383c6f227a3b8d8d1c71",
"/js/ace.js": "/js/ace.js?id=4a28163d5fd63e64d6af",
"/js/activity.js": "/js/activity.js?id=7405cc1a22814a5b2a70",
"/js/app.js": "/js/app.js?id=2f034c84c06dbb3e511d",
@ -8,18 +8,19 @@
"/css/appdark.css": "/css/appdark.css?id=1b13fc163fa4deb9233f",
"/css/landing.css": "/css/landing.css?id=31de3e75de8690f7ece5",
"/css/quill.css": "/css/quill.css?id=81604d62610b0dbffad6",
"/js/collectioncompose.js": "/js/collectioncompose.js?id=ec48ebc94ae6b1ec70ea",
"/js/components.js": "/js/components.js?id=d64d41a2defb1a205865",
"/js/compose.js": "/js/compose.js?id=488b24bf2f2168540449",
"/js/developers.js": "/js/developers.js?id=ba029a560f0c30c5efc9",
"/js/discover.js": "/js/discover.js?id=627f0a106fd359ae3b80",
"/js/hashtag.js": "/js/hashtag.js?id=0f7e529e8128cc17638b",
"/js/loops.js": "/js/loops.js?id=19112dc8663fc43db735",
"/js/mode-dot.js": "/js/mode-dot.js?id=c7c83849e6bba99f1c33",
"/js/profile.js": "/js/profile.js?id=e285b20ac6d467f99138",
"/js/quill.js": "/js/quill.js?id=c2b060eaf87ef63eb5c1",
"/js/search.js": "/js/search.js?id=3186ccb02c7fad43c701",
"/js/status.js": "/js/status.js?id=4b9a52b586880e264f05",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=a4da64fc6e2f406a616d",
"/js/timeline.js": "/js/timeline.js?id=a3c3e38470108fa5df42"
"/js/collectioncompose.js": "/js/collectioncompose.js?id=200765234feeb3b1351c",
"/js/collections.js": "/js/collections.js?id=93bac411f11eb701648f",
"/js/components.js": "/js/components.js?id=7e4df37c02f12db5ef96",
"/js/compose.js": "/js/compose.js?id=df5bd23aef5b73027cce",
"/js/developers.js": "/js/developers.js?id=a395f12c52bb0eada6ab",
"/js/discover.js": "/js/discover.js?id=f8da29f2b16ae5be93fd",
"/js/hashtag.js": "/js/hashtag.js?id=b4ffe6499880acf0591c",
"/js/loops.js": "/js/loops.js?id=214f31fc6c2d990487d8",
"/js/mode-dot.js": "/js/mode-dot.js?id=8224e306cf53e3336620",
"/js/profile.js": "/js/profile.js?id=56aca6209960f8ac2110",
"/js/quill.js": "/js/quill.js?id=9edfe94c043a1bc68860",
"/js/search.js": "/js/search.js?id=b1bd588d07e682f8fce5",
"/js/status.js": "/js/status.js?id=248bb6c2bec534e81503",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=344fb8527bb66574e4cd",
"/js/timeline.js": "/js/timeline.js?id=b894c6fe644d16ffc330"
}

View file

@ -0,0 +1,4 @@
Vue.component(
'collection-compose',
require('./components/CollectionCompose.vue').default
);

4
resources/assets/js/collections.js vendored Normal file
View file

@ -0,0 +1,4 @@
Vue.component(
'collection-component',
require('./components/CollectionComponent.vue').default
);

View file

@ -0,0 +1,69 @@
<template>
<div>
<div class="row">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in posts">
<a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="previewBackground(s)">
</div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.favourites_count}}</span>
</span>
<span>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.reblogs_count}}</span>
</span>
</h5>
</div>
</div>
</a>
</div>
</div>
</div>
</template>
<style type="text/css" scoped></style>
<script type="text/javascript">
export default {
props: ['collection-id'],
data() {
return {
loaded: false,
posts: [],
}
},
beforeMount() {
this.fetchItems();
},
mounted() {
},
methods: {
fetchItems() {
axios.get('/api/local/collection/items/' + this.collectionId)
.then(res => {
this.posts = res.data;
});
},
previewUrl(status) {
return status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].preview_url;
},
previewBackground(status) {
let preview = this.previewUrl(status);
return 'background-image: url(' + preview + ');';
},
}
}
</script>

View file

@ -0,0 +1,257 @@
<template>
<div class="container">
<div v-if="loaded" class="row">
<div class="col-12 col-md-6 offset-md-3 pt-5">
<div class="text-center pb-4">
<h1>Create Collection</h1>
</div>
</div>
<div class="col-12 col-md-4 pt-3">
<div class="card rounded-0 shadow-none border " style="min-height: 440px;">
<div class="card-body">
<div>
<form>
<div class="form-group">
<label for="title" class="font-weight-bold text-muted">Title</label>
<input type="text" class="form-control" id="title" placeholder="Collection Title" v-model="collection.title">
</div>
<div class="form-group">
<label for="description" class="font-weight-bold text-muted">Description</label>
<textarea class="form-control" id="description" placeholder="Example description here" v-model="collection.description" rows="3">
</textarea>
</div>
<div class="form-group">
<label for="visibility" class="font-weight-bold text-muted">Visibility</label>
<select class="custom-select" v-model="collection.visibility">
<option value="public">Public</option>
<option value="private">Followers Only</option>
</select>
</div>
</form>
<hr>
<p>
<button type="button" class="btn btn-primary font-weight-bold btn-block" @click="publish">Publish</button>
</p>
<p>
<button type="button" class="btn btn-outline-primary font-weight-bold btn-block" @click="save">Save</button>
</p>
<p class="mb-0">
<button type="button" class="btn btn-outline-secondary font-weight-bold btn-block" @click="deleteCollection">Delete</button>
</p>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-8 pt-3">
<div>
<ul class="nav nav-tabs">
<li class="nav-item">
<a :class="[tab == 'add' ? 'nav-link font-weight-bold bg-white active' : 'nav-link font-weight-bold text-muted']" href="#" @click.prevent="tab = 'add'">Add Posts</a>
</li>
<li class="nav-item">
<a :class="[tab == 'all' ? 'nav-link font-weight-bold bg-white active' : 'nav-link font-weight-bold text-muted']" href="#" @click.prevent="tab = 'all'">Preview</a>
</li>
</ul>
</div>
<div class="card rounded-0 shadow-none border border-top-0">
<div class="card-body" style="height: 460px; overflow-y: auto">
<div v-if="tab == 'all'" class="row">
<div class="col-4 p-1" v-for="(s, index) in posts">
<a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="previewBackground(s)">
</div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.favourites_count}}</span>
</span>
<span>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.reblogs_count}}</span>
</span>
</h5>
</div>
</div>
</a>
</div>
</div>
<div v-if="tab == 'add'">
<div class="form-group">
<label for="title" class="font-weight-bold text-muted">Add Post by URL</label>
<input type="text" class="form-control" placeholder="https://pixelfed.dev/p/admin/1" v-model="id">
<p class="help-text small text-muted">Only local, public posts can be added</p>
</div>
<div class="form-group pt-4">
<label for="title" class="font-weight-bold text-muted">Add Recent Post</label>
<div>
<div v-for="(s, index) in recentPosts" :class="[selectedPost == s.id ? 'box-shadow border border-warning d-inline-block m-1':'d-inline-block m-1']" @click="selectPost(s)">
<div class="cursor-pointer" :style="'width: 175px; height: 175px; ' + previewBackground(s)"></div>
</div>
</div>
</div>
<hr>
<button type="button" class="btn btn-primary font-weight-bold btn-block" @click="addId">Add Post</button>
</div>
<div v-if="tab == 'order'">
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
export default {
props: ['collection-id', 'profile-id'],
data() {
return {
loaded: false,
limit: 8,
step: 1,
title: '',
description: '',
visibility: 'private',
collection: {
title: '',
description: '',
visibility: 'public'
},
id: '',
posts: [],
tab: 'add',
tabs: [
'all',
'add',
'order'
],
recentPosts: [],
selectedPost: '',
}
},
beforeMount() {
axios.get('/api/local/collection/' + this.collectionId)
.then(res => {
this.collection = res.data;
});
},
mounted() {
this.fetchRecentPosts();
this.fetchItems();
},
methods: {
addToIds(id) {
axios.post('/api/local/collection/item', {
collection_id: this.collectionId,
post_id: id
}).then(res => {
this.fetchItems();
this.fetchRecentPosts();
this.tab = 'all';
this.id = '';
}).catch(err => {
swal('Invalid URL', 'The post you entered was invalid', 'error');
this.id = '';
})
},
fetchItems() {
axios.get('/api/local/collection/items/' + this.collectionId)
.then(res => {
this.posts = res.data;
this.loaded = true;
});
},
addId() {
let max = 18;
if(this.posts.length >= max) {
swal('Error', 'You can only add ' + max + ' posts per collection', 'error');
return;
}
let url = this.id;
let origin = window.location.origin;
let split = url.split('/');
if(url.slice(0, origin.length) !== origin) {
swal('Invalid URL', 'You can only add posts from this instance', 'error');
this.id = '';
}
if(url.slice(0, origin.length + 3) !== origin + '/p/' || split.length !== 6) {
swal('Invalid URL', 'Invalid URL', 'error');
this.id = '';
}
this.addToIds(split[5]);
return;
},
previewUrl(status) {
return status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].preview_url;
},
previewBackground(status) {
let preview = this.previewUrl(status);
return 'background-image: url(' + preview + ');background-size:cover;';
},
fetchRecentPosts() {
axios.get('/api/v1/accounts/' + this.profileId + '/statuses', {
params: {
only_media: true,
min_id: 1,
}
}).then(res => {
this.recentPosts = res.data.filter(s => {
let ids = this.posts.map(s => {
return s.id;
});
return s.visibility == 'public' && s.sensitive == false && ids.indexOf(s.id) == -1;
}).slice(0,3);
});
},
selectPost(status) {
this.selectedPost = status.id;
this.id = status.url;
},
publish() {
axios.post('/api/local/collection/' + this.collectionId + '/publish', {
title: this.collection.title,
description: this.collection.description,
visibility: this.collection.visibility
})
.then(res => {
window.location.href = res.data;
});
},
save() {
axios.post('/api/local/collection/' + this.collectionId, {
title: this.collection.title,
description: this.collection.description,
visibility: this.collection.visibility
})
.then(res => {
swal('Saved!', 'You have successfully saved this collection.', 'success');
});
},
deleteCollection() {
let confirm = window.confirm('Are you sure you want to delete this collection?');
if(!confirm) {
return;
}
axios.delete('/api/local/collection/' + this.collectionId)
.then(res => {
window.location.href = '/';
});
}
}
}
</script>

View file

@ -14,9 +14,9 @@
<span class="fas fa-ellipsis-v fa-lg text-muted"></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<div v-show="media.length > 0" class="dropdown-item small font-weight-bold" v-on:click="mediaDrawer = !mediaDrawer">{{mediaDrawer ? 'Hide' : 'Show'}} Media Toolbar</div>
<div class="dropdown-item small font-weight-bold" v-on:click="about">About</div>
<div class="dropdown-item small font-weight-bold" v-on:click="createCollection">Create Collection</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item small font-weight-bold" v-on:click="about">About</div>
<div class="dropdown-item small font-weight-bold" v-on:click="closeModal">Close</div>
</div>
</div>
@ -507,6 +507,10 @@ export default {
return video ?
'Click here to add photos or videos' :
'Click here to add photos';
},
createCollection() {
window.location.href = '/i/collections/create';
}
}
}

View file

@ -152,9 +152,9 @@
<li class="nav-item px-3">
<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
</li>
<!-- <li class="nav-item pr-3">
<li class="nav-item pr-3">
<a :class="this.mode == 'collections' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('collections')"><i class="fas fa-images fa-lg"></i></a>
</li> -->
</li>
<li class="nav-item" v-if="owner">
<a :class="this.mode == 'bookmarks' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('bookmarks')"><i class="fas fa-bookmark fa-lg"></i></a>
</li>
@ -189,7 +189,7 @@
</div>
</div>
<div class="row" v-if="mode == 'list'">
<div class="col-md-8 col-lg-8 offset-md-2 px-0 mb-3 timeline">
<div class="col-md-8 col-lg-8 offset-md-2 px-0 timeline">
<div class="card status-card card-md-rounded-0 my-sm-2 my-md-3 my-lg-4" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
<div class="card-header d-inline-flex align-items-center bg-white">
@ -282,6 +282,12 @@
</div>
</div>
</div>
<div v-if="['grid','list'].indexOf(mode) != -1 && timeline.length == 0">
<div class="py-5 text-center text-muted">
<p><i class="fas fa-camera-retro fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">No posts yet</p>
</div>
</div>
<div v-if="timeline.length && ['grid','list'].indexOf(mode) != -1">
<infinite-loading @infinite="infiniteTimeline">
<div slot="no-more"></div>
@ -289,32 +295,55 @@
</infinite-loading>
</div>
<div class="row" v-if="mode == 'bookmarks'">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in bookmarks">
<a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="previewBackground(s)">
<div v-if="bookmarks.length">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(s, index) in bookmarks">
<a class="card info-overlay card-md-border-0" :href="s.url">
<div class="square">
<span v-if="s.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
<span v-if="s.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
<span v-if="s.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
<div class="square-content" v-bind:style="previewBackground(s)">
</div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.favourites_count}}</span>
</span>
<span>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.reblogs_count}}</span>
</span>
</h5>
</div>
</div>
<div class="info-overlay-text">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.favourites_count}}</span>
</span>
<span>
<span class="fas fa-retweet fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{s.reblogs_count}}</span>
</span>
</h5>
</div>
</div>
</a>
</a>
</div>
</div>
<div v-else class="col-12">
<div class="py-5 text-center text-muted">
<p><i class="fas fa-bookmark fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">You have no saved bookmarks</p>
</div>
</div>
</div>
<div class="row" v-if="mode == 'collections'">
<p class="text-center">Collections here</p>
<div class="col-12" v-if="mode == 'collections'">
<div v-if="collections.length" class="row">
<div class="col-4 p-0 p-sm-2 p-md-3 p-xs-1" v-for="(c, index) in collections">
<a class="card info-overlay card-md-border-0" :href="c.url">
<div class="square">
<div class="square-content" v-bind:style="'background-image: url(' + c.thumb + ');'">
</div>
</div>
</a>
</div>
</div>
<div v-else>
<div class="py-5 text-center text-muted">
<p><i class="fas fa-images fa-2x"></i></p>
<p class="h2 font-weight-light pt-3">No collections yet</p>
</div>
</div>
</div>
</div>
</div>
@ -688,6 +717,12 @@
this.bookmarks = res.data
});
}
if(this.mode == 'collections' && this.collections.length == 0) {
axios.get('/api/local/profile/collections/' + this.profileId)
.then(res => {
this.collections = res.data
});
}
},
reportProfile() {

View file

@ -0,0 +1,24 @@
@extends('layouts.app')
@section('content')
<collection-compose collection-id="{{$collection->id}}" profile-id="{{Auth::user()->profile_id}}"></collection-compose>
@endsection
@push('styles')
<style type="text/css">
</style>
@endpush
@push('scripts')
<script type="text/javascript" src="{{ mix('js/collectioncompose.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">
$(document).ready(function() {
new Vue({
el: '#content'
});
});
</script>
@endpush

View file

@ -0,0 +1,33 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-12 mt-5 py-5">
<div class="text-center">
<h1>Collection</h1>
<h4 class="text-muted">{{$collection->title}}</h4>
</div>
</div>
<div class="col-12">
<collection-component collection-id="{{$collection->id}}"></collection-component>
</div>
</div>
</div>
@endsection
@push('styles')
<style type="text/css">
</style>
@endpush
@push('scripts')
<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
<script type="text/javascript" src="{{mix('js/collections.js')}}"></script>
<script type="text/javascript">
new Vue({
el: '#content'
})
</script>
@endpush

View file

@ -115,6 +115,13 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/tag/list', 'HashtagFollowController@getTags');
Route::get('profile/sponsor/{id}', 'ProfileSponsorController@get');
Route::get('bookmarks', 'InternalApiController@bookmarks');
Route::get('collection/items/{id}', 'CollectionController@getItems');
Route::post('collection/item', 'CollectionController@storeId');
Route::get('collection/{id}', 'CollectionController@get');
Route::post('collection/{id}', 'CollectionController@store');
Route::delete('collection/{id}', 'CollectionController@delete');
Route::post('collection/{id}/publish', 'CollectionController@publish')->middleware('throttle:maxCollectionsPerHour,60')->middleware('throttle:maxCollectionsPerDay,1440')->middleware('throttle:maxCollectionsPerMonth,43800');
Route::get('profile/collections/{id}', 'CollectionController@getUserCollections');
});
});
@ -167,6 +174,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('abusive/post', 'ReportController@abusivePostForm')->name('report.abusive.post');
Route::get('abusive/profile', 'ReportController@abusiveProfileForm')->name('report.abusive.profile');
});
Route::get('collections/create', 'CollectionController@create');
});
Route::group(['prefix' => 'account'], function () {
@ -314,6 +323,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('{username}/following', 'FederationController@userFollowing');
});
Route::get('c/{collection}', 'CollectionController@show');
Route::get('p/{username}/{id}/c/{cid}', 'CommentController@show');
Route::get('p/{username}/{id}/c', 'CommentController@showAll');
Route::get('p/{username}/{id}/edit', 'StatusController@edit');

3
webpack.mix.js vendored
View file

@ -31,6 +31,9 @@ mix.js('resources/assets/js/app.js', 'public/js')
// .js('resources/assets/js/embed.js', 'public')
// .js('resources/assets/js/direct.js', 'public/js')
.js('resources/assets/js/hashtag.js', 'public/js')
.js('resources/assets/js/collectioncompose.js', 'public/js')
.js('resources/assets/js/collections.js', 'public/js')
.extract([
'lodash',
'popper.js',