functional version
|
|
@ -2,17 +2,41 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Requests\StoreFolderRequest;
|
|
||||||
use App\Http\Requests\StoreFileRequest;
|
use App\Http\Requests\NewFolder;
|
||||||
|
use App\Http\Requests\UploadFiles;
|
||||||
|
use App\Http\Requests\SelectedFiles;
|
||||||
|
use App\Http\Requests\RecycledFiles;
|
||||||
|
use App\Http\Requests\ShareFiles;
|
||||||
|
use App\Http\Requests\SharedFiles;
|
||||||
|
|
||||||
use App\Http\Resources\FileResource;
|
use App\Http\Resources\FileResource;
|
||||||
use App\Models\File;
|
|
||||||
use Inertia\Inertia;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
use App\Models\File;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\FileShare;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
use Kalnoy\Nestedset\Collection;
|
||||||
|
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
|
use App\Mail\ShareFilesNotification;
|
||||||
|
|
||||||
class FileController extends Controller
|
class FileController extends Controller
|
||||||
{
|
{
|
||||||
// render user's files
|
// render user's files
|
||||||
public function userFiles(string $folder = null) {
|
public function userFiles(Request $request, string $folder = null) {
|
||||||
// validate passed in $folder param
|
// validate passed in $folder param
|
||||||
// if passed in,
|
// if passed in,
|
||||||
if ($folder) {
|
if ($folder) {
|
||||||
|
|
@ -25,18 +49,25 @@ class FileController extends Controller
|
||||||
// use root folder if $folder not passed in
|
// use root folder if $folder not passed in
|
||||||
$folder = $this->getRoot();
|
$folder = $this->getRoot();
|
||||||
}
|
}
|
||||||
// get all files and folders where parent_id = root
|
|
||||||
$files = File::query()->where('parent_id', $folder->id)
|
// get all files and folders created by current user
|
||||||
// that are created by the current user
|
$files = File::query()->where('created_by', Auth::id())
|
||||||
->where('created_by', Auth::id())
|
|
||||||
// that have not been deleted
|
// that have not been deleted
|
||||||
->whereNull('deleted_at')
|
->whereNull('deleted_at')
|
||||||
// order by folders first
|
// order by folders first
|
||||||
->orderByDesc('is_folder')
|
->orderByDesc('is_folder')
|
||||||
// then order by creation date
|
// then order by creation date
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at');
|
||||||
// return x amount (default 8)
|
|
||||||
->paginate(8);
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$files->where('name', 'like', $search);
|
||||||
|
} else { // return all files filtered by parent_id
|
||||||
|
$files->where('parent_id', $folder->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $files->get();
|
||||||
|
|
||||||
$files = FileResource::collection($files);
|
$files = FileResource::collection($files);
|
||||||
|
|
||||||
|
|
@ -47,30 +78,838 @@ class FileController extends Controller
|
||||||
return Inertia::render('UserFiles', compact('files', 'folder', 'ancestors'));
|
return Inertia::render('UserFiles', compact('files', 'folder', 'ancestors'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new folder based on passed in StoreFolderRequest
|
// render files shared with user
|
||||||
public function newFolder(StoreFolderRequest $request) {
|
public function sharedWithMe(Request $request, $folder = null) {
|
||||||
// get validated data
|
// validate passed in $folder param
|
||||||
$data = $request->validated();
|
if ($folder) {
|
||||||
// get parent folder from StoreFolderRequest
|
// get ancestors
|
||||||
$parent = $request->parent;
|
$firstQuery = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
// check for parent
|
->where('file_shares.user_id', Auth::id())
|
||||||
if (!$parent) {
|
->whereNull('files.deleted_at')
|
||||||
$parent = $this->getRoot();
|
->find(intval($folder)); // ensure that the specified id exists in the database
|
||||||
|
if ($firstQuery) {
|
||||||
|
$ancestors = $firstQuery->ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder found in the shares db
|
||||||
|
$folder = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
->where('file_shares.user_id', Auth::id())
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'parent_id')
|
||||||
|
->find(intval($folder)); // ensure that the specified id exists in the database
|
||||||
|
}
|
||||||
|
// if still exists,
|
||||||
|
if ($folder) {
|
||||||
|
// files are those found in the shares db
|
||||||
|
$files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
// that are shared with the current user
|
||||||
|
->where('file_shares.user_id', Auth::id())
|
||||||
|
// that have not been deleted
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
// order by folders first
|
||||||
|
->orderByDesc('files.is_folder')
|
||||||
|
// then order by creation date
|
||||||
|
->orderByDesc('files.id')
|
||||||
|
// then select to reference file_id
|
||||||
|
->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id');
|
||||||
|
|
||||||
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$files->where('name', 'like', $search);
|
||||||
|
} else { // return all files filtered by $folder
|
||||||
|
$files->where('parent_id', $folder->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $files->get();
|
||||||
|
|
||||||
|
$files = FileResource::collection($files);
|
||||||
|
|
||||||
|
// ancestors
|
||||||
|
$ancestors = FileResource::collection($ancestors);
|
||||||
|
|
||||||
|
$folder = new FileResource($folder);
|
||||||
|
|
||||||
|
// pass everything into SharedWith
|
||||||
|
return Inertia::render('SharedWith', compact('files', 'folder', 'ancestors'));
|
||||||
|
}
|
||||||
|
else { // no folder
|
||||||
|
// files are those found in the shares db
|
||||||
|
$files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
// that are shared with the current user
|
||||||
|
->where('file_shares.user_id', Auth::id())
|
||||||
|
// that have not been deleted
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
// order by folders first
|
||||||
|
->orderByDesc('files.is_folder')
|
||||||
|
// then order by creation date
|
||||||
|
->orderByDesc('files.id')
|
||||||
|
->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id');
|
||||||
|
|
||||||
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$files->where('name', 'like', $search);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $files->get();
|
||||||
|
|
||||||
|
// make into collection
|
||||||
|
$files = FileResource::collection($files);
|
||||||
|
|
||||||
|
$allIds = array();
|
||||||
|
|
||||||
|
// get $parentIds of each file
|
||||||
|
foreach($files as $file) {
|
||||||
|
array_push($allIds, intval($file->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// run through and filter the $files array to not have any files which are children of the displayed files
|
||||||
|
foreach($files as $key => $value) {
|
||||||
|
foreach($allIds as $allId) {
|
||||||
|
if ($value->parent_id == $allId) {
|
||||||
|
unset($files[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass only files into SharedWith
|
||||||
|
return Inertia::render('SharedWith', compact('files'));
|
||||||
}
|
}
|
||||||
$file = new File();
|
|
||||||
$file->is_folder = 1;
|
|
||||||
$file->name = $data['name'];
|
|
||||||
$parent->appendNode($file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function upload(StoreFileRequest $request) {
|
// render files shared by user
|
||||||
|
public function sharedByMe(Request $request, $folder = null) {
|
||||||
|
// validate passed in $folder param
|
||||||
|
if ($folder) {
|
||||||
|
// run a query just for ancestors
|
||||||
|
$firstQuery = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
->where('files.created_by', Auth::id())
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
->find(intval($folder)); // ensure that the specified id exists in the database
|
||||||
|
if ($firstQuery) {
|
||||||
|
$ancestors = $firstQuery->ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder found in the shares db
|
||||||
|
$folder = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
->where('files.created_by', Auth::id())
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'parent_id')
|
||||||
|
->find(intval($folder)); // ensure that the specified id exists in the database
|
||||||
|
}
|
||||||
|
// if still exists,
|
||||||
|
if ($folder) {
|
||||||
|
// files are those found in the shares db
|
||||||
|
$files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
// that are made by the current user
|
||||||
|
->where('files.created_by', Auth::id())
|
||||||
|
// that have not been deleted
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
// order by folders first
|
||||||
|
->orderByDesc('files.is_folder')
|
||||||
|
// then order by creation date
|
||||||
|
->orderByDesc('files.id')
|
||||||
|
// then select to reference file_id
|
||||||
|
->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id', 'file_shares.user_id as user_id');
|
||||||
|
|
||||||
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$files->where('name', 'like', $search);
|
||||||
|
} else { // return all files filtered by $folder
|
||||||
|
$files->where('parent_id', $folder->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $files->get();
|
||||||
|
|
||||||
|
$files = FileResource::collection($files);
|
||||||
|
|
||||||
|
// get shared_with name for each file
|
||||||
|
foreach($files as $file) {
|
||||||
|
$user = User::query()->where('id', $file->user_id)->first();
|
||||||
|
$file->shared_with = $user->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ancestors
|
||||||
|
$ancestors = FileResource::collection($ancestors);
|
||||||
|
|
||||||
|
$folder = new FileResource($folder);
|
||||||
|
|
||||||
|
// pass everything into SharedBy
|
||||||
|
return Inertia::render('SharedBy', compact('files', 'folder', 'ancestors'));
|
||||||
|
}
|
||||||
|
else { // no folder
|
||||||
|
// files are those found in the shares db
|
||||||
|
$files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
// that are made by the current user
|
||||||
|
->where('files.created_by', Auth::id())
|
||||||
|
// that have not been deleted
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
// order by folders first
|
||||||
|
->orderByDesc('files.is_folder')
|
||||||
|
// then order by creation date
|
||||||
|
->orderByDesc('files.id')
|
||||||
|
->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id', 'file_shares.user_id as user_id');
|
||||||
|
|
||||||
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$files->where('name', 'like', $search);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $files->get();
|
||||||
|
|
||||||
|
// get shared_with name for each file
|
||||||
|
foreach($files as $file) {
|
||||||
|
$user = User::query()->where('id', $file->user_id)->first();
|
||||||
|
$file->shared_with = $user->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make into collection
|
||||||
|
$files = FileResource::collection($files);
|
||||||
|
|
||||||
|
$allIds = array();
|
||||||
|
|
||||||
|
// get $parentIds of each file
|
||||||
|
foreach($files as $file) {
|
||||||
|
array_push($allIds, intval($file->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// run through and filter the $files array to not have any files which are children of the displayed files
|
||||||
|
foreach($files as $key => $value) {
|
||||||
|
foreach($allIds as $allId) {
|
||||||
|
if ($value->parent_id == $allId) {
|
||||||
|
unset($files[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass only files into SharedBy
|
||||||
|
return Inertia::render('SharedBy', compact('files'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// render recycle bin
|
||||||
|
public function recycleBin(Request $request, $folder = null) {
|
||||||
|
// validate passed in $folder param
|
||||||
|
if ($folder) {
|
||||||
|
// find folder
|
||||||
|
$folder = File::onlyTrashed()
|
||||||
|
// made by current user
|
||||||
|
->where('created_by', Auth::id())
|
||||||
|
->find(intval($folder));
|
||||||
|
}
|
||||||
|
// if still exists,
|
||||||
|
if ($folder) {
|
||||||
|
// files are those that are trashed
|
||||||
|
$files = File::onlyTrashed()
|
||||||
|
// made by current user
|
||||||
|
->where('created_by', Auth::id())
|
||||||
|
->orderBy('is_folder', 'desc')
|
||||||
|
->orderBy('deleted_at', 'desc')
|
||||||
|
->orderBy('files.id', 'desc');
|
||||||
|
|
||||||
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$files->where('name', 'like', $search);
|
||||||
|
} else { // return all files filtered by $folder
|
||||||
|
$files->where('parent_id', $folder->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $files->get();
|
||||||
|
|
||||||
|
$files = FileResource::collection($files);
|
||||||
|
|
||||||
|
function recursiveAncestors($file, &$ancestors) {
|
||||||
|
$parentFile = File::onlyTrashed()
|
||||||
|
->where('created_by', Auth::id())
|
||||||
|
->find($file->parent_id);
|
||||||
|
|
||||||
|
if ($parentFile) {
|
||||||
|
array_unshift($ancestors, $parentFile);
|
||||||
|
if ($parentFile->parent_id) {
|
||||||
|
recursiveAncestors($parentFile, $ancestors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ancestors = [];
|
||||||
|
recursiveAncestors($folder, $ancestors);
|
||||||
|
// ancestors
|
||||||
|
$ancestors = FileResource::collection([...$ancestors, $folder]);
|
||||||
|
|
||||||
|
$folder = new FileResource($folder);
|
||||||
|
|
||||||
|
// pass everything into RecycleBin
|
||||||
|
return Inertia::render('RecycleBin', compact('files', 'folder', 'ancestors'));
|
||||||
|
}
|
||||||
|
else { // no folder
|
||||||
|
// files are those that are trashed
|
||||||
|
$files = File::onlyTrashed()
|
||||||
|
// made by current user
|
||||||
|
->where('created_by', Auth::id())
|
||||||
|
->orderBy('is_folder', 'desc')
|
||||||
|
->orderBy('deleted_at', 'desc')
|
||||||
|
->orderBy('files.id', 'desc');
|
||||||
|
|
||||||
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$files->where('name', 'like', $search);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $files->get();
|
||||||
|
|
||||||
|
// make into collection
|
||||||
|
$files = FileResource::collection($files);
|
||||||
|
|
||||||
|
$allIds = array();
|
||||||
|
|
||||||
|
// get $parentIds of each file
|
||||||
|
foreach($files as $file) {
|
||||||
|
array_push($allIds, intval($file->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// run through and filter the $files array to not have any files which are children of the displayed files
|
||||||
|
foreach($files as $key => $value) {
|
||||||
|
foreach($allIds as $allId) {
|
||||||
|
if ($value->parent_id == $allId) {
|
||||||
|
unset($files[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass only files into SharedBy
|
||||||
|
return Inertia::render('RecycleBin', compact('files'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new folder based on passed in NewFolder request
|
||||||
|
public function newFolder(NewFolder $request) {
|
||||||
// get validated data
|
// get validated data
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
// get parent folder from StoreFolderRequest
|
// get parent folder from NewFolder request
|
||||||
dd ($data);
|
$parent = $request->parent;
|
||||||
|
// make new folder object based on File() object
|
||||||
|
$folder = new File();
|
||||||
|
$folder->is_folder = 1;
|
||||||
|
$folder->name = $data['name'];
|
||||||
|
|
||||||
|
$parent->appendNode($folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upload files
|
||||||
|
public function upload(UploadFiles $request) {
|
||||||
|
// get validated data
|
||||||
|
$data = $request->validated();
|
||||||
|
// get parent folder from UploadFiles request
|
||||||
|
$parent = $request->parent;
|
||||||
|
// get file tree structure from UploadFiles request
|
||||||
|
$tree = $request->file_tree;
|
||||||
|
// if the tree is empty, then files were uploaded without any folder structure - each file needs to be uploaded
|
||||||
|
// otherwise, the entire folder structure needs to be uploaded
|
||||||
|
empty($tree) ? $this->uploadFiles($data['files'], $parent) : $this->uploadTree($tree, $parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get download link for selected files
|
||||||
|
public function download(SelectedFiles $request) {
|
||||||
|
$data = $request->validated();
|
||||||
|
// get parent folder from DeleteFiles request
|
||||||
|
$parent = $request->parent;
|
||||||
|
// get selected Id's otherwise empty array
|
||||||
|
$Ids = $data['Ids'] ?? [];
|
||||||
|
|
||||||
|
// if no files selected
|
||||||
|
if (!$data['all'] == false && empty($Ids)) {
|
||||||
|
return [
|
||||||
|
'message' => 'No files selected!'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all selected, download all
|
||||||
|
if ($data['all']) {
|
||||||
|
$downloadURL = $this->makeArchive($parent->children);
|
||||||
|
// make filename for file
|
||||||
|
$filename = $parent->name . ".zip";
|
||||||
|
}
|
||||||
|
// else download the selected IDs
|
||||||
|
else {
|
||||||
|
// if there is only one ID selected
|
||||||
|
if (count($Ids) === 1) {
|
||||||
|
// get file
|
||||||
|
$file = File::find($Ids[0]);
|
||||||
|
// if file is a folder
|
||||||
|
if ($file->is_folder) {
|
||||||
|
// and folder is empty
|
||||||
|
if($file->children->isEmpty()) {
|
||||||
|
// return message
|
||||||
|
return [
|
||||||
|
"message" => "This folder is empty"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// else folder is populated
|
||||||
|
$files = $file->children;
|
||||||
|
$downloadURL = $this->makeArchive($files);
|
||||||
|
$filename = $file->name . ".zip";
|
||||||
|
}
|
||||||
|
// otherwise
|
||||||
|
else {
|
||||||
|
// create a new path off "public/"
|
||||||
|
$path = "public/" . pathinfo($file->stored_at, PATHINFO_BASENAME);
|
||||||
|
// copy file to path
|
||||||
|
Storage::copy($file->stored_at, $path);
|
||||||
|
// define download path
|
||||||
|
$downloadURL = asset(Storage::url($path));
|
||||||
|
// define filename
|
||||||
|
$filename = $file->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise there are many IDs selected
|
||||||
|
else {
|
||||||
|
$files = File::query()->whereIn('id', $Ids)->get();
|
||||||
|
$downloadURL = $this->makeArchive($files);
|
||||||
|
$filename = $parent->name . ".zip";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return URL and filename
|
||||||
|
return [
|
||||||
|
'url' => $downloadURL,
|
||||||
|
'filename' => $filename,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get download link for files that are shared
|
||||||
|
public function downloadShared(SharedFiles $request) {
|
||||||
|
$data = $request->validated();
|
||||||
|
// get parent folder from DeleteFiles request
|
||||||
|
$parent = $request->parent;
|
||||||
|
// get selected Id's otherwise empty array
|
||||||
|
$Ids = $data['Ids'] ?? [];
|
||||||
|
// if no files selected
|
||||||
|
if (!$data['all'] == false && empty($Ids)) {
|
||||||
|
return [
|
||||||
|
'message' => 'No files selected!'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parent) {
|
||||||
|
// if all selected, download all
|
||||||
|
if ($data['all']) {
|
||||||
|
$downloadURL = $this->makeArchive($parent->children);
|
||||||
|
// make filename for file
|
||||||
|
$filename = $parent->name . ".zip";
|
||||||
|
}
|
||||||
|
|
||||||
|
// else download the selected IDs
|
||||||
|
else {
|
||||||
|
// if there is only one ID selected
|
||||||
|
if (count($Ids) === 1) {
|
||||||
|
// get file
|
||||||
|
$file = File::find($Ids[0]);
|
||||||
|
// if file is a folder
|
||||||
|
if ($file->is_folder) {
|
||||||
|
// and folder is empty
|
||||||
|
if ($file->children->isEmpty()) {
|
||||||
|
// return message
|
||||||
|
return [
|
||||||
|
"message" => "This folder is empty"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// else folder is populated
|
||||||
|
$files = $file->children;
|
||||||
|
$downloadURL = $this->makeArchive($files);
|
||||||
|
$filename = $file->name . ".zip";
|
||||||
|
}
|
||||||
|
// otherwise
|
||||||
|
else {
|
||||||
|
// create a new path off "public/"
|
||||||
|
$path = "public/" . pathinfo($file->stored_at, PATHINFO_BASENAME);
|
||||||
|
// copy file to path
|
||||||
|
Storage::copy($file->stored_at, $path);
|
||||||
|
// define download path
|
||||||
|
$downloadURL = asset(Storage::url($path));
|
||||||
|
// define filename
|
||||||
|
$filename = $file->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise there are many IDs selected
|
||||||
|
else {
|
||||||
|
$files = File::query()->whereIn('id', $Ids)->get();
|
||||||
|
$downloadURL = $this->makeArchive($files);
|
||||||
|
$filename = $parent->name . ".zip";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else there is no $parent
|
||||||
|
else {
|
||||||
|
// if all selected, download all
|
||||||
|
if ($data['all']) {
|
||||||
|
// files are those found in the shares db
|
||||||
|
$files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
// that are shared with the current user
|
||||||
|
->where('file_shares.user_id', Auth::id())
|
||||||
|
// that have not been deleted
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
// order by folders first
|
||||||
|
->orderByDesc('files.is_folder')
|
||||||
|
// then order by creation date
|
||||||
|
->orderByDesc('files.id')
|
||||||
|
->get();
|
||||||
|
$downloadURL = $this->makeArchive($files);
|
||||||
|
// make filename for file
|
||||||
|
$filename = "shared-" . Str::random(10) . ".zip";
|
||||||
|
}
|
||||||
|
|
||||||
|
// else download the selected IDs
|
||||||
|
else {
|
||||||
|
// if there is only one ID selected
|
||||||
|
if (count($Ids) === 1) {
|
||||||
|
// get file
|
||||||
|
$file = File::find($Ids[0]);
|
||||||
|
// if file is a folder
|
||||||
|
if ($file->is_folder) {
|
||||||
|
// and folder is empty
|
||||||
|
if ($file->children->isEmpty()) {
|
||||||
|
// return message
|
||||||
|
return [
|
||||||
|
"message" => "This folder is empty"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// else folder is populated
|
||||||
|
$files = $file->children;
|
||||||
|
$downloadURL = $this->makeArchive($files);
|
||||||
|
$filename = $file->name . ".zip";
|
||||||
|
}
|
||||||
|
// otherwise
|
||||||
|
else {
|
||||||
|
// create a new path off "public/"
|
||||||
|
$path = "public/" . pathinfo($file->stored_at, PATHINFO_BASENAME);
|
||||||
|
// copy file to path
|
||||||
|
Storage::copy($file->stored_at, $path);
|
||||||
|
// define download path
|
||||||
|
$downloadURL = asset(Storage::url($path));
|
||||||
|
// define filename
|
||||||
|
$filename = $file->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise there are many IDs selected
|
||||||
|
else {
|
||||||
|
$files = File::query()->whereIn('id', $Ids)->get();
|
||||||
|
$downloadURL = $this->makeArchive($files);
|
||||||
|
$filename = "shared-" . Str::random(10) . ".zip";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return URL and filename
|
||||||
|
return [
|
||||||
|
'url' => $downloadURL,
|
||||||
|
'filename' => $filename,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// recycle selected files
|
||||||
|
public function recycle(SelectedFiles $request) {
|
||||||
|
// get validated data
|
||||||
|
$data = $request->validated();
|
||||||
|
// get parent folder from DeleteFiles request
|
||||||
|
$parent = $request->parent;
|
||||||
|
|
||||||
|
// if all files were selected
|
||||||
|
if ($data['all']) {
|
||||||
|
foreach ($parent->children as $file) {
|
||||||
|
$file->recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise specific IDs were selected
|
||||||
|
else {
|
||||||
|
foreach($data['Ids'] ?? [] as $fileId) {
|
||||||
|
$targetFile = File::find($fileId);
|
||||||
|
if ($targetFile) $targetFile->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('userFiles', [
|
||||||
|
'folder' => $parent->path
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// restores selected files
|
||||||
|
public function restore(RecycledFiles $request) {
|
||||||
|
// get validated data
|
||||||
|
$data = $request->validated();
|
||||||
|
|
||||||
|
// if all files were selected
|
||||||
|
if ($data['all']) {
|
||||||
|
$files = File::onlyTrashed()->get();
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$file->restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise specific IDs were selected
|
||||||
|
else {
|
||||||
|
// get Id's unless empty
|
||||||
|
$Ids = $data['Ids'] ?? [];
|
||||||
|
|
||||||
|
// get files
|
||||||
|
$files = File::onlyTrashed()
|
||||||
|
->whereIn('id', $Ids)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$file->restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('recycleBin');
|
||||||
|
}
|
||||||
|
|
||||||
|
// deletes selected files (permanent delete)
|
||||||
|
public function delete(RecycledFiles $request) {
|
||||||
|
// get validated data
|
||||||
|
$data = $request->validated();
|
||||||
|
|
||||||
|
// if all files were selected
|
||||||
|
if ($data['all']) {
|
||||||
|
$files = File::onlyTrashed()->get();
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$file->shred();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise specific IDs were selected
|
||||||
|
else {
|
||||||
|
// get Id's unless empty
|
||||||
|
$Ids = $data['Ids'] ?? [];
|
||||||
|
|
||||||
|
// get files
|
||||||
|
$files = File::onlyTrashed()
|
||||||
|
->whereIn('id', $Ids)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$file->shred();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('recycleBin');
|
||||||
|
}
|
||||||
|
|
||||||
|
// shares selected files
|
||||||
|
public function share(ShareFiles $request) {
|
||||||
|
// get data
|
||||||
|
$data = $request->validated();
|
||||||
|
// get parent folder
|
||||||
|
$parent = $request->parent;
|
||||||
|
// get selected Id's otherwise empty array
|
||||||
|
$Ids = $data['Ids'] ?? [];
|
||||||
|
// get user by email
|
||||||
|
$user = User::query()->where('email', $data['email'])->first();
|
||||||
|
|
||||||
|
// if no files selected
|
||||||
|
if (!$data['all'] == false && empty($Ids)) {
|
||||||
|
return [
|
||||||
|
'message' => 'No files selected!'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if user doesn't exist
|
||||||
|
if (!$user->id) {
|
||||||
|
return [
|
||||||
|
'message' => 'Email does not belong to user!'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, if all is passed, get current children. else find files by passed Id's
|
||||||
|
$files = $data['all'] ? $parent->children : File::find($Ids);
|
||||||
|
|
||||||
|
// define recursive function to add files to share
|
||||||
|
function recursiveShare($queued, $recipient) {
|
||||||
|
// get children of parent and determine whether recursive function needs to be called
|
||||||
|
foreach ($queued as $file) {
|
||||||
|
// check if file already exists in shared DB
|
||||||
|
$exists = FileShare::query()->where('file_id', $file->id)->where('user_id', $recipient->id)->first();
|
||||||
|
if ($exists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// else
|
||||||
|
// create new entry
|
||||||
|
$entry = [
|
||||||
|
'file_id' => $file->id,
|
||||||
|
'user_id' => $recipient->id,
|
||||||
|
'created_at' => $file->created_at,
|
||||||
|
'updated_at' => Carbon::now(),
|
||||||
|
];
|
||||||
|
FileShare::insert($entry);
|
||||||
|
|
||||||
|
// recurse on children, if any
|
||||||
|
if ($file->children) {
|
||||||
|
recursiveShare($file->children, $recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin recursion on files
|
||||||
|
recursiveShare($files, $user);
|
||||||
|
|
||||||
|
// send email to recipient
|
||||||
|
Mail::to($user)->send(new ShareFilesNotification($user, Auth::user(), $files));
|
||||||
|
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// unshares selected files
|
||||||
|
public function unshare(SharedFiles $request) {
|
||||||
|
// get data
|
||||||
|
$data = $request->validated();
|
||||||
|
// get parent folder
|
||||||
|
$parent = $request->parent;
|
||||||
|
|
||||||
|
// get selected Id's otherwise empty array
|
||||||
|
$Ids = $data['Ids'] ?? [];
|
||||||
|
|
||||||
|
// if no files selected
|
||||||
|
if (!$data['all'] == false && empty($Ids)) {
|
||||||
|
return [
|
||||||
|
'message' => 'No files selected!'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if parent exists
|
||||||
|
if ($parent) {
|
||||||
|
// if all is passed, get current children. else find files by passed Id's
|
||||||
|
$files = $data['all'] ? $parent->children : File::find($Ids);
|
||||||
|
}
|
||||||
|
// else
|
||||||
|
else {
|
||||||
|
// find all files that are created by current user and shared with others
|
||||||
|
$all = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||||
|
->where('files.created_by', Auth::id())
|
||||||
|
->whereNull('files.deleted_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$files = $data['all'] ? $all : File::find($Ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
// define recursive function to remove files to share
|
||||||
|
function recursiveUnshare($queued) {
|
||||||
|
// get children of parent and determine whether recursive function needs to be called
|
||||||
|
foreach ($queued as $file) {
|
||||||
|
// check if file already exists in shared DB
|
||||||
|
$dbRecord = FileShare::query()->where('file_id', $file->id)->first();
|
||||||
|
if ($dbRecord) {
|
||||||
|
// get regular db file
|
||||||
|
$file = File::query()->where('created_by', Auth::id())->find($file->id);
|
||||||
|
$dbRecord->forceDelete();
|
||||||
|
|
||||||
|
// recurse on children, if any
|
||||||
|
if ($file->children) {
|
||||||
|
recursiveUnshare($file->children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// begin recursion on files
|
||||||
|
recursiveUnshare($files);
|
||||||
|
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets user's root folder - called from userFiles
|
||||||
private function getRoot() {
|
private function getRoot() {
|
||||||
return File::query()->whereIsRoot()->where('created_by', Auth::id())->firstOrFail();
|
return File::query()->whereIsRoot()->where('created_by', Auth::id())->firstOrFail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upload files - called from upload
|
||||||
|
private function uploadFiles($files, $parent) {
|
||||||
|
// loop over files, make and upload each one
|
||||||
|
foreach ($files as $userFile) {
|
||||||
|
$file = new File();
|
||||||
|
$file->stored_at = $userFile->store('/files/'.Auth::id());
|
||||||
|
$file->is_folder = false;
|
||||||
|
$file->name = $userFile->getClientOriginalName();
|
||||||
|
$file->mimetype = $userFile->getMimeType();
|
||||||
|
$file->size = $userFile->getSize();
|
||||||
|
$parent->appendNode($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload tree - called from upload
|
||||||
|
private function uploadTree($tree, $parent) {
|
||||||
|
// loop over files in file tree
|
||||||
|
foreach($tree as $name => $file) {
|
||||||
|
// if current node in $tree is an array (that means, a folder)
|
||||||
|
if (is_array($file)) {
|
||||||
|
// make folder
|
||||||
|
$folder = new File();
|
||||||
|
$folder->is_folder = 1;
|
||||||
|
$folder->name = $name;
|
||||||
|
// append folder to parent
|
||||||
|
$parent->appendNode($folder);
|
||||||
|
// call function recursively until if condition is false
|
||||||
|
// each call has the newly-created $folder as $parent
|
||||||
|
$this->uploadTree($file, $folder);
|
||||||
|
}
|
||||||
|
// otherwise the node is a file; use uploadFiles() with file
|
||||||
|
else {
|
||||||
|
$this->uploadFiles([$file], $parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makes archive - called from download
|
||||||
|
private function makeArchive(Collection $files): string
|
||||||
|
{
|
||||||
|
// make name for archive
|
||||||
|
$archiveName = Str::random() . ".zip";
|
||||||
|
// create public path
|
||||||
|
$path = "public/$archiveName";
|
||||||
|
|
||||||
|
// if the current $path is NOT already being used
|
||||||
|
if (!is_dir(dirname($path))) {
|
||||||
|
Storage::makeDirectory(dirname($path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the absolute path of $path
|
||||||
|
$path = Storage::path($path);
|
||||||
|
// create new ZipArchive
|
||||||
|
$archive = new ZipArchive();
|
||||||
|
// see if a archive can be created or overwritten at $path
|
||||||
|
$res = $archive->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||||||
|
// if so
|
||||||
|
if ($res === true) {
|
||||||
|
// define recursive function to add files to archive
|
||||||
|
function recursiveAdd($queued, $archive, $parent = "") {
|
||||||
|
// get children of parent and determine whether recursive function needs to be called
|
||||||
|
foreach ($queued as $file) {
|
||||||
|
// if there are no children, delete file
|
||||||
|
if ($file->children->isEmpty()) {
|
||||||
|
// set $internalPath to reflect file
|
||||||
|
$internalPath = $parent . $file->name;
|
||||||
|
// add file to archive
|
||||||
|
$archive->addFile(Storage::path($file->stored_at), $internalPath);
|
||||||
|
// continue loop
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// else
|
||||||
|
// set $internalPath to reflect a directory
|
||||||
|
$internalPath = $parent . $file->name . "/";
|
||||||
|
// recurse
|
||||||
|
recursiveAdd($file->children, $archive, $internalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add all $files to $archive
|
||||||
|
recursiveAdd($files, $archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
// close archive
|
||||||
|
$archive->close();
|
||||||
|
|
||||||
|
// return archive
|
||||||
|
return asset(Storage::url($archiveName));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
use App\Http\Requests\ParentIDBaseRequest;
|
use App\Http\Requests\ParentId;
|
||||||
use App\Models\File;
|
use App\Models\File;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class StoreFolderRequest extends ParentIDBaseRequest
|
class NewFolder extends ParentId
|
||||||
{
|
{
|
||||||
// authorization handled by ParentIDBaseRequest
|
// authorization handled by ParentId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
|
|
@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Auth;
|
||||||
use App\Models\File;
|
use App\Models\File;
|
||||||
|
|
||||||
|
|
||||||
class ParentIDBaseRequest extends FormRequest
|
class ParentId extends FormRequest
|
||||||
{
|
{
|
||||||
public ?File $parent = null;
|
public ?File $parent = null;
|
||||||
|
|
||||||
33
app/Http/Requests/RecycledFiles.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class RecycledFiles extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
// append rules to default validation rules
|
||||||
|
return [
|
||||||
|
'all' => 'bool', // all or not
|
||||||
|
'Ids.*' => Rule::exists('files', 'id') // check that file exists in database
|
||||||
|
->where(fn($query) => $query->where('created_by', Auth::id())) // check file was made by current user
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Http/Requests/SelectedFiles.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Http\Requests\ParentId;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class SelectedFiles extends ParentId
|
||||||
|
{
|
||||||
|
// auth handled by ParentId
|
||||||
|
|
||||||
|
protected function prepareForValidation() {
|
||||||
|
// cast to boolean
|
||||||
|
$this->merge([
|
||||||
|
'all' => filter_var($this->all, FILTER_VALIDATE_BOOL),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
// append rules to default validation rules
|
||||||
|
return array_merge(parent::rules(), [
|
||||||
|
'all' => 'bool', // all or not
|
||||||
|
'Ids.*' => Rule::exists('files', 'id') // check that file exists in database
|
||||||
|
->where(fn($query) => $query->where('created_by', Auth::id())) // check file was made by current user
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Http/Requests/ShareFiles.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Http\Requests\SelectedFiles;
|
||||||
|
|
||||||
|
class ShareFiles extends SelectedFiles
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
// append rules to validation rules
|
||||||
|
return array_merge(parent::rules(), [
|
||||||
|
'email' => 'required|email', // email or not
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Http/Requests/SharedFiles.php
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
use App\Models\File;
|
||||||
|
|
||||||
|
class SharedFiles extends FormRequest
|
||||||
|
{
|
||||||
|
public ?File $parent = null;
|
||||||
|
private ?File $match = null;
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
// gets first file (main folder) with parent_id as id
|
||||||
|
$this->parent = File::query()->where('id', $this->input('parent_id'))->first();
|
||||||
|
// continue
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation() {
|
||||||
|
// cast to boolean
|
||||||
|
$this->merge([
|
||||||
|
'all' => filter_var($this->all, FILTER_VALIDATE_BOOL),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
if ($this->parent) {
|
||||||
|
// validate
|
||||||
|
return [
|
||||||
|
'parent_id' => [
|
||||||
|
Rule::exists(File::class, 'id')
|
||||||
|
->where(function (Builder $query) {
|
||||||
|
return $query
|
||||||
|
->where('is_folder', '1'); // must be a folder
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
'all' => 'bool', // all or not
|
||||||
|
'Ids.*' => Rule::exists('files', 'id'), // check that files exist in database
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [
|
||||||
|
'all' => 'bool', // all or not
|
||||||
|
'Ids.*' => Rule::exists('files', 'id'), // check that file exists in database
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
|
|
||||||
use App\Http\Requests\ParentIDBaseRequest;
|
|
||||||
use App\Models\File;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
class StoreFileRequest extends ParentIDBaseRequest
|
|
||||||
{
|
|
||||||
// authorization handled by ParentIDBaseRequest
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return array_merge(parent::rules(),
|
|
||||||
[
|
|
||||||
'files.*' => [
|
|
||||||
'required',
|
|
||||||
'file',
|
|
||||||
function ($attribute, $value, $fail) {
|
|
||||||
$file = File::query()
|
|
||||||
->where('name', $value->getClientOriginalName())
|
|
||||||
->where('created_by', Auth::id())
|
|
||||||
->where('parent_id', $this->parent_id)
|
|
||||||
->whereNull('deleted_at');
|
|
||||||
|
|
||||||
if ($file->exists()) {
|
|
||||||
$fail('File already exists.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
126
app/Http/Requests/UploadFiles.php
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
|
||||||
|
use App\Http\Requests\ParentId;
|
||||||
|
use App\Models\File;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class UploadFiles extends ParentId
|
||||||
|
{
|
||||||
|
// authorization handled by ParentId
|
||||||
|
// validation
|
||||||
|
|
||||||
|
protected function prepareForValidation() {
|
||||||
|
$paths = array_filter($this->paths ?? [],
|
||||||
|
fn($f) => $f != null
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->merge([
|
||||||
|
'file_paths' => $paths,
|
||||||
|
'folder_name' => $this->getFolder($paths)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return array_merge(parent::rules(),
|
||||||
|
[
|
||||||
|
'files.*' => [
|
||||||
|
'required',
|
||||||
|
'file',
|
||||||
|
function ($attribute, $value, $fail) {
|
||||||
|
// if this is not a folder, apply file validation to file
|
||||||
|
if (!$this->folder_name) {
|
||||||
|
// find if a file with the same parameters already exists
|
||||||
|
$file = File::query()
|
||||||
|
->where('name', $value->getClientOriginalName())
|
||||||
|
->where('created_by', Auth::id())
|
||||||
|
->where('parent_id', $this->parent_id)
|
||||||
|
->whereNull('deleted_at');
|
||||||
|
|
||||||
|
if ($file->exists()) {
|
||||||
|
$fail('File already exists.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'folder_name' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
function ($attribute, $value, $fail) {
|
||||||
|
// if the folder name was passed in, that is if the path was not empty
|
||||||
|
if ($value) {
|
||||||
|
// find if a folder with the same parameters already exists
|
||||||
|
$folder = File::query()
|
||||||
|
->where('name', $value)
|
||||||
|
->where('created_by', Auth::id())
|
||||||
|
->where('parent_id', $this->parent_id)
|
||||||
|
->whereNull('deleted_at');
|
||||||
|
|
||||||
|
if ($folder->exists()) {
|
||||||
|
$fail('Folder already exists.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function passedValidation() {
|
||||||
|
// get validated data
|
||||||
|
$data = $this->validated();
|
||||||
|
|
||||||
|
// replace object with file tree (function below)
|
||||||
|
$this->replace([
|
||||||
|
'file_tree' => $this->makeTree($this->file_paths, $data['files'])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns folder path for uploaded file
|
||||||
|
public function getFolder($paths) {
|
||||||
|
if (!$paths) return null;
|
||||||
|
$url = explode("/", $paths[0]);
|
||||||
|
return $url[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// makes file tree from paths and files
|
||||||
|
private function makeTree($paths, $files) {
|
||||||
|
// match $paths array count to $files array count in case there is a difference between the two
|
||||||
|
$paths = array_slice($paths, 0, count($files));
|
||||||
|
// filter away empty values
|
||||||
|
$paths = array_filter($paths, fn($path) => $path != null);
|
||||||
|
// build the tree
|
||||||
|
$fileTree = [];
|
||||||
|
|
||||||
|
foreach ($paths as $index => $path) {
|
||||||
|
// get full path as array of url components separated by slashes
|
||||||
|
$url = explode("/", $path);
|
||||||
|
$node = &$fileTree; // get node on file tree - passed by reference for modification
|
||||||
|
|
||||||
|
foreach ($url as $urlIndex => $urlComponent) {
|
||||||
|
// if there is no node value at the current $urlIndex, make empty array for value
|
||||||
|
// example: /test/file.png
|
||||||
|
// $node[$urlComponent] at test becomes empty array holding the files inside
|
||||||
|
if(!isset($node[$urlComponent])) {
|
||||||
|
$node[$urlComponent] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if at the last index in $url, the current index is the uploaded file itself
|
||||||
|
// file can be found at the files array at the same index as $index since $paths has been filtered
|
||||||
|
// otherwise, set $node to current node and continue loop
|
||||||
|
$urlIndex == (count($url) - 1) ? $node[$urlComponent] = $files[$index] : $node = &$node[$urlComponent];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return completed $fileTree after loop complete and nodes are made for every file
|
||||||
|
return $fileTree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,20 +16,36 @@ class FileResource extends JsonResource
|
||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
|
// get deleted at
|
||||||
|
$deletedAt = $this->deleted_at;
|
||||||
|
// make date readable if it is not null
|
||||||
|
if ($deletedAt) {
|
||||||
|
$deletedAt = $deletedAt->diffForHumans();
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = $this->size;
|
||||||
|
// get if folder
|
||||||
|
$isFolder = $this->is_folder;
|
||||||
|
// if it is, set $size to getFolderSize()
|
||||||
|
if ($isFolder) {
|
||||||
|
$size = $this->getFolderSize();
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"mime" => $this->mime,
|
"mime" => $this->mimetype,
|
||||||
"path" => $this->path,
|
"path" => $this->path,
|
||||||
"id" => $this->id,
|
"id" => $this->id,
|
||||||
"parent_id" => $this->parent_id,
|
"parent_id" => $this->parent_id,
|
||||||
"name" => $this->name,
|
"name" => $this->name,
|
||||||
"size" => $this->size,
|
"size" => $this->formatSize($size),
|
||||||
"owner" => $this->user->name,
|
"owner" => $this->user->name,
|
||||||
"is_folder" => $this->is_folder,
|
"is_folder" => $this->is_folder,
|
||||||
"created_at" => $this->created_at->diffForHumans(),
|
"created_at" => $this->created_at->diffForHumans(),
|
||||||
"updated_at" => $this->updated_at->diffForHumans(),
|
"updated_at" => $this->updated_at->diffForHumans(),
|
||||||
"created_by" => $this->created_by,
|
"created_by" => $this->created_by,
|
||||||
"updated_by" => $this->updated_by,
|
"updated_by" => $this->updated_by,
|
||||||
"deleted_at" => $this->deleted_at,
|
"deleted_at" => $deletedAt,
|
||||||
|
"shared_with" => $this->shared_with ? $this->shared_with : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
app/Mail/ShareFilesNotification.php
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
|
||||||
|
class ShareFilesNotification extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*/
|
||||||
|
public function __construct(public User $recipient, public User $sender, public $files)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message envelope.
|
||||||
|
*/
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Files Shared',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message content definition.
|
||||||
|
*/
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
view: 'mail.share',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attachments for the message.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||||
|
*/
|
||||||
|
public function attachments(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Kalnoy\Nestedset\NodeTrait;
|
use Kalnoy\Nestedset\NodeTrait;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Kalnoy\NestedSet\DescendantsRelation;
|
||||||
|
|
||||||
class File extends Model
|
class File extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -36,6 +39,35 @@ class File extends Model
|
||||||
return $this->created_by = $userID;
|
return $this->created_by = $userID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns formatted size
|
||||||
|
public function formatSize($size) {
|
||||||
|
// maximum file size is in the GB range
|
||||||
|
$sizeUnits = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
// floor() the size and get the amount of times 1024 fits into it
|
||||||
|
$power = $size > 0 ? floor(log($size, 1024)) : 0;
|
||||||
|
// calculate output by dividing total size by $power * 1024 then adding relevant $sizeUnit based on $power ($power can be no greater than 3 - no bigger than GB's)
|
||||||
|
$output = number_format(($size / pow(1024, $power)), 2, ".", ",") . " " . $sizeUnits[$power];
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputs correct sizes for folders
|
||||||
|
public function getFolderSize() {
|
||||||
|
$sum = [];
|
||||||
|
// call recursive function, then sum
|
||||||
|
$this->recurseChildren($this->children, $sum);
|
||||||
|
$sum = array_sum($sum);
|
||||||
|
// return $sum
|
||||||
|
return $sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// shred $this file
|
||||||
|
public function shred() {
|
||||||
|
// recursively shred all of $this
|
||||||
|
$this->recursiveShred([$this]);
|
||||||
|
// also call force delete
|
||||||
|
$this->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
// additional bootstrapping on top of default Model model
|
// additional bootstrapping on top of default Model model
|
||||||
protected static function boot()
|
protected static function boot()
|
||||||
{
|
{
|
||||||
|
|
@ -58,5 +90,52 @@ class File extends Model
|
||||||
// append current file or folder name to path name
|
// append current file or folder name to path name
|
||||||
$model->path = $model->path . Str::slug($model->name);
|
$model->path = $model->path . Str::slug($model->name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// define delete function
|
||||||
|
static::deleted(function (File $file) {
|
||||||
|
// if file is a regular file (not a folder)
|
||||||
|
if (!$file->is_folder) {
|
||||||
|
// delete file from file system
|
||||||
|
Storage::delete($file->stored_at);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves $this file to recycle bin
|
||||||
|
private function recycle() {
|
||||||
|
// set this file's deleted_at to now
|
||||||
|
$this->deleted_at = Carbon::now();
|
||||||
|
// save record
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively shred $queued files
|
||||||
|
private function recursiveShred($queued) {
|
||||||
|
// get children of parent and determine whether recursive function needs to be called
|
||||||
|
foreach ($queued as $file) {
|
||||||
|
// if there are no children, delete file
|
||||||
|
if ($file->children->isEmpty()) {
|
||||||
|
Storage::delete($file->stored_at);
|
||||||
|
// continue loop
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// else recurse
|
||||||
|
$this->recursiveShred($file->children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all children and subchildren
|
||||||
|
private function recurseChildren ($files, &$total) {
|
||||||
|
// go through each file in queue
|
||||||
|
foreach($files as $file) {
|
||||||
|
// push to array since it is a child
|
||||||
|
array_push($total, $file->size);
|
||||||
|
// iterate to next file in $queue if no more children
|
||||||
|
if (!$file->children) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// else recurse on $current file children
|
||||||
|
$this->recurseChildren($file->children, $total);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,11 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
class FileShare extends Model
|
class FileShare extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'file_id',
|
||||||
|
'user_id',
|
||||||
|
'created_at',
|
||||||
|
'updated_at'
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
public const HOME = '/my-files';
|
public const HOME = '/files';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define your route model bindings, pattern filters, and other route configuration.
|
* Define your route model bindings, pattern filters, and other route configuration.
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ return new class extends Migration
|
||||||
{
|
{
|
||||||
Schema::create('starred_files', function (Blueprint $table) {
|
Schema::create('starred_files', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('fileID')->constrained('files');
|
$table->foreignId('file_id')->constrained('files');
|
||||||
$table->foreignId('userID')->constrained('users');
|
$table->foreignId('user_id')->constrained('users');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('files', function (Blueprint $table) {
|
||||||
|
$table->string('stored_at', 2000)->nullable()->after('path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('files', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(('stored_at'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
services:
|
services:
|
||||||
laravel.test:
|
laravel.test:
|
||||||
build:
|
build:
|
||||||
context: ./vendor/laravel/sail/runtimes/8.2
|
context: ./docker/8.2
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
WWWGROUP: '${WWWGROUP}'
|
WWWGROUP: '${WWWGROUP}'
|
||||||
|
|
@ -40,7 +40,7 @@ services:
|
||||||
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||||
volumes:
|
volumes:
|
||||||
- 'sail-mysql:/var/lib/mysql'
|
- 'sail-mysql:/var/lib/mysql'
|
||||||
- './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
- './docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||||
networks:
|
networks:
|
||||||
- sail
|
- sail
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
|
||||||
63
docker/8.2/Dockerfile
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=18
|
||||||
|
ARG POSTGRES_VERSION=15
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils librsvg2-bin fswatch \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y php8.2-cli php8.2-dev \
|
||||||
|
php8.2-pgsql php8.2-sqlite3 php8.2-gd php8.2-imagick \
|
||||||
|
php8.2-curl \
|
||||||
|
php8.2-imap php8.2-mysql php8.2-mbstring \
|
||||||
|
php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \
|
||||||
|
php8.2-intl php8.2-readline \
|
||||||
|
php8.2-ldap \
|
||||||
|
php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \
|
||||||
|
php8.2-memcached php8.2-pcov php8.2-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y mysql-client \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2
|
||||||
|
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
8
docker/8.2/php.ini
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[PHP]
|
||||||
|
post_max_size = 2G
|
||||||
|
upload_max_filesize = 2G
|
||||||
|
max_file_uploads = 500
|
||||||
|
variables_order = EGPCS
|
||||||
|
|
||||||
|
[opcache]
|
||||||
|
opcache.enable_cli=1
|
||||||
17
docker/8.2/start-container
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
14
docker/8.2/supervisord.conf
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80
|
||||||
|
user=sail
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
6
docker/mysql/create-testing-database.sh
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS testing;
|
||||||
|
GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
|
||||||
|
EOSQL
|
||||||
2
docker/pgsql/create-testing-database.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
SELECT 'CREATE DATABASE testing'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec
|
||||||
64
package-lock.json
generated
|
|
@ -5,8 +5,13 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@headlessui/vue": "^1.7.16",
|
"@headlessui/vue": "^1.7.16",
|
||||||
"@heroicons/vue": "^2.0.18",
|
"@heroicons/vue": "^2.0.18",
|
||||||
|
"fontawesome-free": "^1.0.4",
|
||||||
"mitt": "^3.0.1"
|
"mitt": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -397,6 +402,60 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/vue-fontawesome": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||||
|
"vue": ">= 3.0.0 < 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@headlessui/vue": {
|
"node_modules/@headlessui/vue": {
|
||||||
"version": "1.7.16",
|
"version": "1.7.16",
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.16.tgz",
|
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.16.tgz",
|
||||||
|
|
@ -1080,6 +1139,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fontawesome-free": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fontawesome-free/-/fontawesome-free-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-7sX6Lbg2oQiClFFFFitJlKg20h3YTBON6rdmq3uGjNwDo8G6EjF2bfj2OjjcCUmf4OvZCgyHaXfW2JseqissLw=="
|
||||||
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,13 @@
|
||||||
"vue": "^3.2.41"
|
"vue": "^3.2.41"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@headlessui/vue": "^1.7.16",
|
"@headlessui/vue": "^1.7.16",
|
||||||
"@heroicons/vue": "^2.0.18",
|
"@heroicons/vue": "^2.0.18",
|
||||||
|
"fontawesome-free": "^1.0.4",
|
||||||
"mitt": "^3.0.1"
|
"mitt": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
public/images/application-octet-stream.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 5,2.5 c -0.554,0 -1,0.446 -1,1 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8.5 L 14.5,8 14,2.5 Z"/>
|
||||||
|
<path fill="#e4e4e4" d="M 5,2 C 4.446,2 4,2.446 4,3 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8 L 14.5,7.5 14,2 Z"/>
|
||||||
|
<path fill="#ffffff" opacity=".2" d="M 5,2 C 4.446,2 4,2.446 4,3 v 0.5 c 0,-0.554 0.446,-1 1,-1 h 9 L 19.5,8 H 20 L 14,2 Z"/>
|
||||||
|
<path style="opacity:0.2" d="m 14,2.5 v 5 c 0,0.5523 0.447715,1 1,1 h 5 z"/>
|
||||||
|
<path fill="#fafafa" d="m 14,2 v 5 c 0,0.5523 0.447715,1 1,1 h 5 z"/>
|
||||||
|
<path style="fill:#727272" d="m 11,11 v 4 h 3 v -4 z m 1,1 h 1 v 2 h -1 z"/>
|
||||||
|
<path style="fill:#727272" d="m 8,6 v 1 h 1 v 3 h 1 V 6 Z"/>
|
||||||
|
<path style="fill:#727272" d="m 11,16 v 4 h 3 v -4 z m 1,1 h 1 v 2 h -1 z"/>
|
||||||
|
<path style="fill:#727272" d="m 8,11 v 1 h 1 v 3 h 1 v -4 z"/>
|
||||||
|
<path style="fill:#727272" d="m 8,16 v 1 h 1 v 3 h 1 v -4 z"/>
|
||||||
|
<path style="fill:#727272" d="m 15,16 v 1 h 1 v 3 h 1 v -4 z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1 KiB |
9
public/images/application-pdf.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 5,2.5 c -0.554,0 -1,0.446 -1,1 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8.5 L 14.5,8 14,2.5 Z"/>
|
||||||
|
<path fill="#c03630" d="m5 2c-0.554 0-1 0.446-1 1v18c0 0.554 0.446 1 1 1h14c0.554 0 1-0.446 1-1v-13l-5.5-0.5-0.5-5.5z"/>
|
||||||
|
<path fill="#fff" style="opacity:0.2" d="m5 2c-0.554 0-1 0.446-1 1v0.5c0-0.554 0.446-1 1-1h9l5.5 5.5h0.5l-6-6z"/>
|
||||||
|
<path style="opacity:0.2" d="m 14,2.5 v 5 c 0,0.5523 0.44772,1 1,1 h 5 z"/>
|
||||||
|
<path fill="#f36961" d="m14 2v5c0 0.5523 0.44772 1 1 1h5l-6-6z"/>
|
||||||
|
<path style="opacity:0.2" d="m 11.39,9.5 c -0.23063,0 -0.4463,0.11548 -0.49848,0.30609 -0.19376,0.73074 0.0231,1.8608 0.38478,3.2688 l -0.1091,0.2726 c -0.27694,0.69058 -0.6231,1.3784 -0.9275,1.9887 -1.2568,2.5156 -2.2344,3.8729 -2.8864,3.968 L 7.3508,19.27657 c -0.01415,-0.31379 0.5519,-1.1228 1.3191,-1.766 0.080025,-0.06621 0.42152,-0.4042 0.42152,-0.4042 0,0 -0.46096,0.24894 -0.5645,0.31313 -0.9614,0.58706 -1.4398,1.1752 -1.5178,1.5657 -0.02315,0.11597 -0.0083,0.25867 0.091875,0.31726 l 0.2458,0.12631 c 0.6692,0.34268 1.492,-0.55836 2.586,-2.5198 1.1132,-0.37358 2.5022,-0.72532 3.7668,-0.9159 1.132,0.66168 2.4305,0.97672 2.9294,0.84071 0.09493,-0.02568 0.1948,-0.10191 0.2458,-0.17213 0.04,-0.0646 0.09591,-0.32313 0.09591,-0.32313 0,0 -0.09386,0.13068 -0.17115,0.1692 -0.31576,0.15248 -1.3126,-0.10191 -2.3356,-0.61391 0.8845,-0.09631 1.6214,-0.10002 2.0152,0.02875 0.50015,0.16332 0.50055,0.33074 0.49389,0.36484 0.0068,-0.02808 0.02915,-0.14024 0.0264,-0.18799 -0.01135,-0.12279 -0.04835,-0.23244 -0.13898,-0.32313 -0.18514,-0.18659 -0.64225,-0.28062 -1.2652,-0.28905 -0.4695,-0.0052 -1.0325,0.03683 -1.6436,0.12631 -0.28006,-0.16452 -0.5756,-0.34538 -0.80975,-0.56931 -0.59385,-0.56741 -1.0916,-1.3552 -1.4007,-2.2384 0.0211,-0.08468 0.0413,-0.1674 0.05972,-0.25087 0.08591,-0.39525 0.14758,-1.702 0.14758,-1.702 0,0 -0.24467,0.98168 -0.28311,1.1298 -0.0247,0.09389 -0.05543,0.19412 -0.09073,0.29845 -0.1875,-0.67412 -0.28254,-1.3275 -0.28254,-1.823 0,-0.14005 0.01175,-0.41256 0.05053,-0.62803 0.0189,-0.15368 0.0733,-0.23349 0.1298,-0.27201 0.11178,0.027724 0.23692,0.20311 0.36754,0.49644 0.11218,0.2536 0.10508,0.54731 0.10508,0.7291 0,0 0.1203,-0.45012 0.09246,-0.71616 -0.017,-0.1599 -0.166,-0.5708 -0.482,-0.566 h -0.02585 l -0.1407,-0.00153 z m 0.10739,4.0814 c 0.32676,0.67212 0.7774,1.3104 1.3686,1.8224 0.13178,0.11396 0.272,0.22238 0.41634,0.3243 -1.0736,0.20425 -2.201,0.49157 -3.2488,0.94061 0.18946,-0.34429 0.3943,-0.71938 0.60415,-1.1239 0.40637,-0.78608 0.6526,-1.3924 0.8597,-1.9634 z"/>
|
||||||
|
<path fill="#fff" d="m11.39 9c-0.23063 0-0.4463 0.11548-0.49848 0.30609-0.19376 0.73074 0.0231 1.8608 0.38478 3.2688l-0.1091 0.2726c-0.27694 0.69058-0.6231 1.3784-0.9275 1.9887-1.2568 2.5156-2.2344 3.8729-2.8864 3.968l-0.0025-0.02762c-0.01415-0.31379 0.5519-1.1228 1.3191-1.766 0.080025-0.06621 0.42152-0.4042 0.42152-0.4042s-0.46096 0.24894-0.5645 0.31313c-0.9614 0.58706-1.4398 1.1752-1.5178 1.5657-0.02315 0.11597-0.0083 0.25867 0.091875 0.31726l0.2458 0.12631c0.6692 0.34268 1.492-0.55836 2.586-2.5198 1.1132-0.37358 2.5022-0.72532 3.7668-0.9159 1.132 0.66168 2.4305 0.97672 2.9294 0.84071 0.09493-0.02568 0.1948-0.10191 0.2458-0.17213 0.04-0.0646 0.09591-0.32313 0.09591-0.32313s-0.09386 0.13068-0.17115 0.1692c-0.31576 0.15248-1.3126-0.10191-2.3356-0.61391 0.8845-0.09631 1.6214-0.10002 2.0152 0.02875 0.50015 0.16332 0.50055 0.33074 0.49389 0.36484 0.0068-0.02808 0.02915-0.14024 0.0264-0.18799-0.01135-0.12279-0.04835-0.23244-0.13898-0.32313-0.18514-0.18659-0.64225-0.28062-1.2652-0.28905-0.4695-0.0052-1.0325 0.03683-1.6436 0.12631-0.28006-0.16452-0.5756-0.34538-0.80975-0.56931-0.59385-0.56741-1.0916-1.3552-1.4007-2.2384 0.0211-0.08468 0.0413-0.1674 0.05972-0.25087 0.08591-0.39525 0.14758-1.702 0.14758-1.702s-0.24467 0.98168-0.28311 1.1298c-0.0247 0.09389-0.05543 0.19412-0.09073 0.29845-0.1875-0.67412-0.28254-1.3275-0.28254-1.823 0-0.14005 0.01175-0.41256 0.05053-0.62803 0.0189-0.15368 0.0733-0.23349 0.1298-0.27201 0.11178 0.027724 0.23692 0.20311 0.36754 0.49644 0.11218 0.2536 0.10508 0.54731 0.10508 0.7291 0 0 0.1203-0.45012 0.09246-0.71616-0.017-0.1599-0.166-0.5708-0.482-0.566h-0.02585l-0.1407-0.00153zm0.10739 4.0814c0.32676 0.67212 0.7774 1.3104 1.3686 1.8224 0.13178 0.11396 0.272 0.22238 0.41634 0.3243-1.0736 0.20425-2.201 0.49157-3.2488 0.94061 0.18946-0.34429 0.3943-0.71938 0.60415-1.1239 0.40637-0.78608 0.6526-1.3924 0.8597-1.9634z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
18
public/images/ark.svg
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<rect style="opacity:0.2" width="20" height="20" x="2" y="2.5" rx="2" ry="2"/>
|
||||||
|
<rect style="fill:#4caf50" width="20" height="20" x="2" y="2" rx="2" ry="2"/>
|
||||||
|
<rect style="opacity:0.2" width="4" height="9.5" x="10" y="2"/>
|
||||||
|
<path style="opacity:0.2;fill:#ffffff" d="M 4,2 C 2.892,2 2,2.892 2,4 v 0.5 c 0,-1.108 0.892,-2 2,-2 h 16 c 1.108,0 2,0.892 2,2 V 4 C 22,2.892 21.108,2 20,2 Z"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="11" y="9"/>
|
||||||
|
<path style="fill:#4b4b4b" d="m 12,9 c 1.5,0 1.5,2 2,2 v 2.5 c 0,0.277 -0.223,0.5 -0.5,0.5 h -3 C 10.223,14 10,13.777 10,13.5 V 11 c 0.5,0 0.5,-2 2,-2 z"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="12" y="8"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="11" y="7"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="12" y="6"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="11" y="5"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="12" y="4"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="11" y="3"/>
|
||||||
|
<rect style="fill:#ffffff" width="1" height="1" x="12" y="2"/>
|
||||||
|
<path style="opacity:0.2" d="m 12,11.5 c -1.105,0 -2,0.9 -2,2 v 4 c 0,1.1 0.895,2 2,2 1.105,0 2,-0.9 2,-2 v -4 c 0,-1.1 -0.895,-2 -2,-2 z m 0,5 a 1,1 0 0 1 1,1 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 z"/>
|
||||||
|
<path style="fill:#ffffff" d="M 12 11 C 10.895 11 10 11.9 10 13 L 10 17 C 10 18.1 10.895 19 12 19 C 13.105 19 14 18.1 14 17 L 14 13 C 14 11.9 13.105 11 12 11 z M 12 16 A 1 1 0 0 1 13 17 A 1 1 0 0 1 12 18 A 1 1 0 0 1 11 17 A 1 1 0 0 1 12 16 z"/>
|
||||||
|
<path style="fill:#909090" d="M 11.992188,10.000061 A 0.50005,0.49772427 0 0 0 11.5,10.50454 v 1.990698 a 0.50005,0.49772427 0 1 0 1,0 V 10.50454 a 0.50005,0.49772427 0 0 0 -0.507812,-0.504479 z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
9
public/images/audio-x-generic.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 5,2.5 c -0.554,0 -1,0.446 -1,1 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8.5 L 14.5,8 14,2.5 Z"/>
|
||||||
|
<path fill="#fe9700" d="m5 2c-0.554 0-1 0.446-1 1v18c0 0.554 0.446 1 1 1h14c0.554 0 1-0.446 1-1v-13l-5.5-0.5-0.5-5.5z"/>
|
||||||
|
<path fill="#fff" style="opacity:0.2" d="m5 2c-0.554 0-1 0.446-1 1v0.5c0-0.554 0.446-1 1-1h9l5.5 5.5h0.5l-6-6z"/>
|
||||||
|
<path style="opacity:0.2" d="m 14,2.5 v 5 c 0,0.5523 0.44772,1 1,1 h 5 z"/>
|
||||||
|
<path fill="#ffbd63" d="m14 2v5c0 0.5523 0.44772 1 1 1h5l-6-6z"/>
|
||||||
|
<path style="opacity:0.2" d="m 10,11.5 v 4.2695 c -0.3039,-0.177 -0.6488,-0.27 -1,-0.27 -1.1046,0 -2,0.89543 -2,2 0,1.10457 0.89543,2 2,2 1.10457,0 2,-0.89543 2,-2 v -4 h 4 v 2.2695 c -0.304,-0.177 -0.649,-0.27 -1,-0.27 -1.1046,0 -2,0.89543 -2,2 0,1.10457 0.89543,2 2,2 1.10457,0 2,-0.89543 2,-2 v -6 h -0.5 z"/>
|
||||||
|
<path fill="#fff" d="m10 11v4.2695c-0.3039-0.177-0.6488-0.27-1-0.27-1.1046 0-2 0.89543-2 2s0.89543 2 2 2 2-0.89543 2-2v-4h4v2.2695c-0.304-0.177-0.649-0.27-1-0.27-1.1046 0-2 0.89543-2 2s0.89543 2 2 2 2-0.89543 2-2v-6h-0.5z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
8
public/images/folder-blue.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<rect style="opacity:0.2" width="20" height="12" x="2" y="9.5" rx="1" ry="1"/>
|
||||||
|
<path style="fill:#4877b1" d="M 2,17 C 2,17.554 2.446,18 3,18 H 21 C 21.554,18 22,17.554 22,17 V 6 C 22,5.446 21.554,5 21,5 H 12 C 10.5,5 10,3 8.5,3 H 3 C 2.446,3 2,3.446 2,4"/>
|
||||||
|
<rect style="opacity:0.2" width="20" height="12" x="2" y="8.5" rx="1" ry="1"/>
|
||||||
|
<rect style="fill:#e4e4e4" width="16" height="8" x="4" y="7" rx="1" ry="1"/>
|
||||||
|
<rect style="fill:#5294e2" width="20" height="12" x="2" y="9" rx="1" ry="1"/>
|
||||||
|
<path style="opacity:0.1;fill:#ffffff" d="M 3,3 C 2.446,3 2,3.446 2,4 V 4.5 C 2,3.946 2.446,3.5 3,3.5 H 8.5 C 10,3.5 10.5,5.5 12,5.5 H 21 C 21.554,5.5 22,5.946 22,6.5 V 6 C 22,5.446 21.554,5 21,5 H 12 C 10.5,5 10,3 8.5,3 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 806 B |
7
public/images/image-x-generic.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 22,20.5 v -16 c 0,-0.554 -0.446,-1 -1,-1 H 3 c -0.554,0 -1,0.446 -1,1 v 16 c 0,0.554 0.446,1 1,1 h 18 c 0.554,0 1,-0.446 1,-1 z"/>
|
||||||
|
<path fill="#36aca3" d="m22 20v-16c0-0.554-0.446-1-1-1h-18c-0.554 0-1 0.446-1 1v16c0 0.554 0.446 1 1 1h18c0.554 0 1-0.446 1-1z"/>
|
||||||
|
<path fill="#fff" style="opacity:0.2" d="m3 3c-0.554 0-1 0.446-1 1v0.5c0-0.554 0.446-1 1-1h18c0.554 0 1 0.446 1 1v-0.5c0-0.554-0.446-1-1-1z"/>
|
||||||
|
<path style="opacity:0.2" d="m 9,10.5 4,6 3,-4 3,4 v 2 H 5 v -4 z"/>
|
||||||
|
<path fill="#fff" d="m9 10 4 6 3-4 3 4v2h-14v-4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 644 B |
8
public/images/text-x-generic.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 5,2.5 c -0.554,0 -1,0.446 -1,1 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8.5 L 14.5,8 14,2.5 Z"/>
|
||||||
|
<path style="fill:#e4e4e4" d="M 5,2 C 4.446,2 4,2.446 4,3 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8 L 14.5,7.5 14,2 Z"/>
|
||||||
|
<path style="opacity:0.2;fill:#ffffff" d="M 5,2 C 4.446,2 4,2.446 4,3 v 0.5 c 0,-0.554 0.446,-1 1,-1 h 9 L 19.5,8 H 20 L 14,2 Z"/>
|
||||||
|
<path style="opacity:0.2" d="m 14,2.5 v 5 c 0,0.5523 0.447715,1 1,1 h 5 z"/>
|
||||||
|
<path style="fill:#fafafa" d="m 14,2 v 5 c 0,0.5523 0.447715,1 1,1 h 5 z"/>
|
||||||
|
<path style="opacity:0.5" d="m 8,18 v -1 h 5 v 1 z m 0,-2 v -1 h 8 v 1 z m 0,-2 v -1 h 8 v 1 z m 0,-2 v -1 h 8 v 1 z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 777 B |
13
public/images/video-x-generic.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 22,20.5 v -16 c 0,-0.554 -0.446,-1 -1,-1 H 3 c -0.554,0 -1,0.446 -1,1 v 16 c 0,0.554 0.446,1 1,1 h 18 c 0.554,0 1,-0.446 1,-1 z"/>
|
||||||
|
<path fill="#7282d9" d="m22 20v-16c0-0.554-0.446-1-1-1h-18c-0.554 0-1 0.446-1 1v16c0 0.554 0.446 1 1 1h18c0.554 0 1-0.446 1-1z"/>
|
||||||
|
<path fill="#fff" opacity=".2" d="m3 3c-0.554 0-1 0.446-1 1v0.5c0-0.554 0.446-1 1-1h18c0.554 0 1 0.446 1 1v-0.5c0-0.554-0.446-1-1-1z"/>
|
||||||
|
<g style="opacity:0.2" transform="translate(1,0.5)">
|
||||||
|
<path d="M 6.5318,8 H 13.467 C 14,8 14,8.5714 14,8.5714 v 6.8571 c 0,0.572 -0.533,0.572 -0.533,0.572 H 6.5334 c 0,0 -0.5334,0 -0.5334,-0.571 V 8.5719 c 0,0 0,-0.5714 0.5334,-0.5714 z"/>
|
||||||
|
<path d="m 17,9 v 6 l -3,-3.1304 z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fff" transform="translate(1)">
|
||||||
|
<path d="m6.5318 8h6.9352c0.533 0 0.533 0.5714 0.533 0.5714v6.8571c0 0.572-0.533 0.572-0.533 0.572h-6.9336s-0.5334 0-0.5334-0.571v-6.8576s0-0.5714 0.5334-0.5714z"/>
|
||||||
|
<path d="m17 9v6l-3-3.1304z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1 KiB |
11
public/images/x-office-document.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 8,12.5 v 1 h 7 v -1 z m 0,2 v 1 h 7 v -1 z m 0,2 v 1 h 7 v -1 z m 0,2 v 1 h 4 v -1 z"/>
|
||||||
|
<path style="opacity:0.2" d="m 5,2.5 c -0.554,0 -1,0.446 -1,1 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8.5 L 14.5,8 14,2.5 Z"/>
|
||||||
|
<path style="fill:#1b83d4" d="M 5,2 C 4.446,2 4,2.446 4,3 V 21 C 4,21.554 4.446,22 5,22 H 19 C 19.554,22 20,21.554 20,21 V 8 L 14.5,7.5 14,2 Z"/>
|
||||||
|
<path fill="#fff" opacity=".1" d="m5 2c-0.554 0-1 0.446-1 1v0.5c0-0.554 0.446-1 1-1h9l5.5 5.5h0.5l-6-6z"/>
|
||||||
|
<path style="opacity:0.2" d="m 14,2.5 v 5 c 0,0.5523 0.44772,1 1,1 h 5 z"/>
|
||||||
|
<path style="fill:#3b9ce6" d="M 14,2 V 7 C 14,7.5523 14.448,8 15,8 H 20 Z"/>
|
||||||
|
<path style="opacity:0.2" d="M 6,11 V 11.5 H 11 V 11 Z M 6,13 V 13.5 H 11 V 13 Z M 6,15 V 15.5 H 11 V 15 Z M 12,15 V 15.5 H 18 V 15 Z M 6,17 V 17.5 H 18 V 17 Z M 6,19 V 19.5 H 11 V 19 Z"/>
|
||||||
|
<path style="fill:#ffffff" d="M 6,10 V 11 H 11 V 10 Z M 6,12 V 13 H 11 V 12 Z M 6,14 V 15 H 11 V 14 Z M 6,16 V 17 H 18 V 16 Z M 6,18 V 19 H 11 V 18 Z"/>
|
||||||
|
<path style="fill:#7ad2f9" d="M 12,10 V 15 H 18 V 10 Z M 16.5,11 A 0.5,0.5 0 0 1 17,11.5 0.5,0.5 0 0 1 16.5,12 0.5,0.5 0 0 1 16,11.5 0.5,0.5 0 0 1 16.5,11 Z M 14,12 16,14 H 13 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
9
public/images/x-office-spreadsheet.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1">
|
||||||
|
<path style="opacity:0.2" d="m 5,2.5 c -0.554,0 -1,0.446 -1,1 v 18 c 0,0.554 0.446,1 1,1 h 14 c 0.554,0 1,-0.446 1,-1 V 8.5 L 14.5,8 14,2.5 Z"/>
|
||||||
|
<path fill="#4bae4f" d="m5 2c-0.554 0-1 0.446-1 1v18c0 0.554 0.446 1 1 1h14c0.554 0 1-0.446 1-1v-13l-5.5-0.5-0.5-5.5z"/>
|
||||||
|
<path fill="#fff" opacity=".1" d="m5 2c-0.554 0-1 0.446-1 1v0.5c0-0.554 0.446-1 1-1h9l5.5 5.5h0.5l-6-6z"/>
|
||||||
|
<path style="opacity:0.2" d="m 14,2.5 v 5 c 0,0.5523 0.44772,1 1,1 h 5 z"/>
|
||||||
|
<path fill="#95cd97" d="m14 2v5c0 0.5523 0.44772 1 1 1h5l-6-6z"/>
|
||||||
|
<path style="opacity:0.2" d="m 8,12.5 v 7 h 7 v -7 z m 1,1 h 2 v 1 H 9 Z m 3,0 h 2 v 1 h -2 z m -3,2 h 2 v 1 H 9 Z m 3,0 h 2 v 1 h -2 z m -3,2 h 2 v 1 H 9 Z m 3,0 h 2 v 1 h -2 z"/>
|
||||||
|
<path fill="#fff" d="m8 12v7h7v-7h-7zm1 1h2v1h-2v-1zm3 0h2v1h-2v-1zm-3 2h2v1h-2v-1zm3 0h2v1h-2v-1zm-3 2h2v1h-2v-1zm3 0h2v1h-2v-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 919 B |
|
|
@ -45,3 +45,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.errormsg {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ const props = defineProps({
|
||||||
|
|
||||||
const classes = computed(() =>
|
const classes = computed(() =>
|
||||||
props.active
|
props.active
|
||||||
? "flex items-center p-3 mb-1 bg-sky-600 rounded-lg font-medium leading-5 text-slate-900 dark:text-gray-100 transition duration-150 ease-in-out hover:ring-sky-500"
|
? "flex items-center p-3 mb-1 bg-sky-600 rounded font-medium leading-5 text-slate-900 dark:text-gray-100 transition duration-150 ease-in-out hover:ring-sky-500"
|
||||||
: "flex items-center p-3 mb-1 rounded-lg font-medium leading-5 text-gray-500 border border-zinc-900 dark:text-gray-400 hover:text-gray-700 dark:hover:text-zinc-100 hover:border-sky-500 hover:ring-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600 transition duration-150 ease-in-out"
|
: "flex items-center p-3 mb-1 rounded font-medium leading-5 text-gray-500 border border-zinc-900 dark:text-gray-400 hover:text-gray-700 dark:hover:text-zinc-100 hover:border-sky-500 hover:ring-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600 transition duration-150 ease-in-out"
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
41
resources/js/Components/custom/DeleteButton.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script setup>
|
||||||
|
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useForm } from "@inertiajs/vue3";
|
||||||
|
|
||||||
|
// creates form to send SelectedFiles Request
|
||||||
|
const form = useForm({
|
||||||
|
parent_id: null,
|
||||||
|
all: null,
|
||||||
|
Ids: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// define props
|
||||||
|
const props = defineProps({
|
||||||
|
wipeall: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// define delete function
|
||||||
|
const onClick = () => {
|
||||||
|
form.all = props.wipeall;
|
||||||
|
form.Ids = props.selected;
|
||||||
|
|
||||||
|
form.delete(route("file.delete"));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="onClick()"
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:bg-red-600 hover:border-red-600 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<TrashIcon class="h-5 w-5" aria-hidden="true" /> Delete
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
128
resources/js/Components/custom/DownloadButton.vue
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<script setup>
|
||||||
|
import { ArrowDownTrayIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { usePage } from "@inertiajs/vue3";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
const page = usePage();
|
||||||
|
|
||||||
|
// define props
|
||||||
|
const props = defineProps({
|
||||||
|
getall: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isShared = ref(false);
|
||||||
|
|
||||||
|
// determine whether this is a shared folder or not
|
||||||
|
onMounted(() => {
|
||||||
|
const regex = new RegExp("shared.*", "i");
|
||||||
|
const shared = regex.test(page.url);
|
||||||
|
if (shared) isShared.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// define download function
|
||||||
|
const download = () => {
|
||||||
|
// do nothing if nothing selected
|
||||||
|
if (!props.getall && props.selected.length === 0) return;
|
||||||
|
|
||||||
|
// create a URL parameter string based on conditions
|
||||||
|
const parameters = new URLSearchParams();
|
||||||
|
// append value of all
|
||||||
|
parameters.append("all", props.getall);
|
||||||
|
// append selected Id's
|
||||||
|
props.selected.forEach((selectedFile) => {
|
||||||
|
parameters.append("Ids[]", selectedFile);
|
||||||
|
});
|
||||||
|
// then append parent_id (current directory)
|
||||||
|
parameters.append("parent_id", page.props.folder.id);
|
||||||
|
|
||||||
|
// define opts
|
||||||
|
const opts = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
// fetch download link
|
||||||
|
fetch(route("file.download") + `?${parameters.toString()}`, opts)
|
||||||
|
// then get response
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
// then get the json
|
||||||
|
.then((json) => {
|
||||||
|
// if response contains no link, return - shouldn't be happening
|
||||||
|
if (!json.url) return;
|
||||||
|
// else create a link to the document for download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = json.url;
|
||||||
|
link.download = json.filename;
|
||||||
|
// then access it
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// define downloadShared function
|
||||||
|
const downloadShared = () => {
|
||||||
|
// do nothing if nothing selected
|
||||||
|
if (!props.getall && props.selected.length === 0) return;
|
||||||
|
|
||||||
|
// create a URL parameter string based on conditions
|
||||||
|
const parameters = new URLSearchParams();
|
||||||
|
// append value of all
|
||||||
|
parameters.append("all", props.getall);
|
||||||
|
// append selected Id's
|
||||||
|
props.selected.forEach((selectedFile) => {
|
||||||
|
parameters.append("Ids[]", selectedFile);
|
||||||
|
});
|
||||||
|
// then append parent_id (current directory) (if one exists)
|
||||||
|
if (page.props.folder) parameters.append("parent_id", page.props.folder.id);
|
||||||
|
|
||||||
|
// define opts
|
||||||
|
const opts = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
// fetch download link
|
||||||
|
fetch(route("file.downloadShared") + `?${parameters.toString()}`, opts)
|
||||||
|
// then get response
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
// then get the json
|
||||||
|
.then((json) => {
|
||||||
|
// if response contains no link, return - shouldn't be happening
|
||||||
|
if (!json.url) return;
|
||||||
|
// else create a link to the document for download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = json.url;
|
||||||
|
link.download = json.filename;
|
||||||
|
// then access it
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
v-if="isShared"
|
||||||
|
@click="downloadShared()"
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:border-sky-600 hover:bg-sky-600 flex flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowDownTrayIcon class="h-5 w-5" aria-hidden="true" /> Download
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
@click="download()"
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:border-sky-600 hover:bg-sky-600 flex flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowDownTrayIcon class="h-5 w-5" aria-hidden="true" /> Download
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
73
resources/js/Components/custom/FileIcon.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
isImage,
|
||||||
|
isAudio,
|
||||||
|
isVideo,
|
||||||
|
isText,
|
||||||
|
isPdf,
|
||||||
|
isDoc,
|
||||||
|
isSpreadsheet,
|
||||||
|
isArchive,
|
||||||
|
} from "@/getMimeType.js";
|
||||||
|
// define props
|
||||||
|
const { file } = defineProps({
|
||||||
|
file: Object,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<img
|
||||||
|
src="../../../../public/images/folder-blue.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
v-if="file.is_folder"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
<img
|
||||||
|
v-if="isImage(file)"
|
||||||
|
src="../../../../public/images/image-x-generic.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isAudio(file)"
|
||||||
|
src="../../../../public/images/audio-x-generic.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isVideo(file)"
|
||||||
|
src="../../../../public/images/video-x-generic.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isPdf(file)"
|
||||||
|
src="../../../../public/images/application-pdf.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isText(file)"
|
||||||
|
src="../../../../public/images/text-x-generic.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isDoc(file)"
|
||||||
|
src="../../../../public/images/x-office-document.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isSpreadsheet(file)"
|
||||||
|
src="../../../../public/images/x-office-spreadsheet.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="isArchive(file)"
|
||||||
|
src="../../../../public/images/ark.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="../../../../public/images/application-octet-stream.svg"
|
||||||
|
class="w-10 h-10"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
40
resources/js/Components/custom/FileUploadErrorModal.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script setup>
|
||||||
|
// define message prop - passed from parent
|
||||||
|
const { message } = defineProps({
|
||||||
|
message: String,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition leave-active-class="duration-200">
|
||||||
|
<div
|
||||||
|
class="fixed top-1/2 left-1/4 overflow-y-auto px-4 py-6 sm:px-0 z-50 w-1/2"
|
||||||
|
scroll-region
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="ease-out duration-300"
|
||||||
|
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave-active-class="ease-in duration-200"
|
||||||
|
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="text-gray-100 border border-sky-600 mb-6 rounded-lg overflow-hidden transform transition-all sm:w-full sm:mx-auto backdrop-blur p-5 flex flex-col"
|
||||||
|
:class="maxWidthClass"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
<h2 class="pt-2 text-xl text-center errormsg">
|
||||||
|
{{ message }}
|
||||||
|
</h2>
|
||||||
|
<div class="justify-center mt-5">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
26
resources/js/Components/custom/MainBreadcrumbButton.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { Link } from "@inertiajs/vue3";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
href: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = computed(() =>
|
||||||
|
props.active
|
||||||
|
? "border-sky-600 border px-3 py-2 rounded bg-sky-600"
|
||||||
|
: "border-gray-700 border px-3 py-2 rounded hover:border-sky-600"
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Link :href="href" :class="classes">
|
||||||
|
<slot />
|
||||||
|
</Link>
|
||||||
|
</template>
|
||||||
|
|
@ -26,12 +26,26 @@ import NavLink from "../NavLink.vue";
|
||||||
<div class="py-5">
|
<div class="py-5">
|
||||||
<NavLink
|
<NavLink
|
||||||
:href="route('userFiles')"
|
:href="route('userFiles')"
|
||||||
:active="$page.url === '/my-files'"
|
:active="new RegExp('files.*', 'gi').test($page.url)"
|
||||||
>My Files</NavLink
|
>My Files</NavLink
|
||||||
>
|
>
|
||||||
<NavLink href="/">Shared with Me</NavLink>
|
<NavLink
|
||||||
<NavLink href="/">Shared by Me</NavLink>
|
:href="route('sharedWith')"
|
||||||
<NavLink href="/">Recycle Bin</NavLink>
|
:active="
|
||||||
|
new RegExp('shared-with-me.*', 'gi').test($page.url)
|
||||||
|
"
|
||||||
|
>Shared with Me</NavLink
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
:href="route('sharedBy')"
|
||||||
|
:active="new RegExp('shared-by-me.*', 'gi').test($page.url)"
|
||||||
|
>Shared by Me</NavLink
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
:href="route('recycleBin')"
|
||||||
|
:active="new RegExp('recycle-bin.*', 'gi').test($page.url)"
|
||||||
|
>Recycle Bin</NavLink
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,36 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
|
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
|
||||||
import NewFolderModal from "./NewFolderModal.vue";
|
import NewFolderModal from "./NewFolderModal.vue";
|
||||||
import UploadFiles from "./UploadFiles.vue";
|
import UploadFiles from "./UploadFiles.vue";
|
||||||
import UploadFolder from "./UploadFolder.vue";
|
import UploadFolder from "./UploadFolder.vue";
|
||||||
|
|
||||||
const newFolderModalActive = ref(false);
|
const newFolderModalActive = ref(false);
|
||||||
|
const rotate = ref(false);
|
||||||
|
|
||||||
const toggleNewFolderModal = (bool) => {
|
const toggleNewFolderModal = (bool) => {
|
||||||
newFolderModalActive.value = bool;
|
newFolderModalActive.value = bool;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleRotate = () => {
|
||||||
|
rotate.value = !rotate.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const plusClass = computed(() =>
|
||||||
|
rotate.value
|
||||||
|
? "rotate-90 transition duration-300 ease-in-out"
|
||||||
|
: "transition duration-300 ease-in-out"
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Menu as="div" class="relative inline-block text-left">
|
<Menu as="div" class="relative inline-block text-left">
|
||||||
<div>
|
<div>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
class="inline-flex w-full justify-center rounded-md bg-green-600/80 px-5 py-3 font-medium text-white hover:bg-green-600/40"
|
class="inline-flex w-full justify-center rounded bg-emerald-700 px-5 py-3 font-medium text-white hover:bg-emerald-700/50"
|
||||||
|
@click="toggleRotate()"
|
||||||
>
|
>
|
||||||
+ New
|
<span :class="plusClass">+</span> New
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -31,14 +43,14 @@ const toggleNewFolderModal = (bool) => {
|
||||||
leave-to-class="transform scale-95 opacity-0"
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
class="border border-gray-300 dark:border-gray-700 absolute left-0 mt-2 w-56 divide-y divide-gray-700 rounded-md bg-zinc-900 focus:outline-none"
|
class="border border-gray-300 dark:border-gray-700 absolute left-0 mt-2 w-56 divide-y divide-gray-700 rounded-md bg-zinc-900 focus:outline-none z-50"
|
||||||
>
|
>
|
||||||
<div class="px-1 py-1">
|
<div class="px-1 py-1">
|
||||||
<MenuItem v-slot="{ active }"
|
<MenuItem v-slot="{ active }"
|
||||||
><a
|
><a
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="toggleNewFolderModal(true)"
|
@click.prevent="toggleNewFolderModal(true)"
|
||||||
class="block w-full items-center rounded-md py-2 border border-zinc-900 block w-full pl-3 pr-4 py-2 text-left text-base font-medium text-gray-600 dark:text-zinc-100 hover:border-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600transition duration-150 ease-in-out"
|
class="block w-full items-center rounded-md py-2 border border-zinc-900 block w-full pl-3 pr-4 py-2 text-left text-base font-medium text-gray-600 dark:text-zinc-100 hover:border-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600 transition duration-150 ease-in-out"
|
||||||
>New Folder</a
|
>New Folder</a
|
||||||
></MenuItem
|
></MenuItem
|
||||||
>
|
>
|
||||||
|
|
|
||||||
49
resources/js/Components/custom/RecycleButton.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script setup>
|
||||||
|
import { faRecycle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { usePage, useForm } from "@inertiajs/vue3";
|
||||||
|
|
||||||
|
const page = usePage();
|
||||||
|
|
||||||
|
// creates form to send SelectedFiles Request
|
||||||
|
const form = useForm({
|
||||||
|
parent_id: null,
|
||||||
|
all: null,
|
||||||
|
Ids: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// define props
|
||||||
|
const props = defineProps({
|
||||||
|
wipeall: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// define delete function
|
||||||
|
const onClick = () => {
|
||||||
|
form.parent_id = page.props.folder.id;
|
||||||
|
form.all = props.wipeall;
|
||||||
|
form.Ids = props.selected;
|
||||||
|
|
||||||
|
form.post(route("file.recycle"));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="onClick()"
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:border-orange-600 hover:bg-orange-600 flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="faRecycle"
|
||||||
|
class="h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Recycle
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
40
resources/js/Components/custom/RestoreButton.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script setup>
|
||||||
|
import { ArrowPathIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useForm } from "@inertiajs/vue3";
|
||||||
|
|
||||||
|
// creates form to send SelectedFiles Request
|
||||||
|
const form = useForm({
|
||||||
|
all: null,
|
||||||
|
Ids: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// define props
|
||||||
|
const props = defineProps({
|
||||||
|
restoreall: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// define delete function
|
||||||
|
const onClick = () => {
|
||||||
|
form.all = props.restoreall;
|
||||||
|
form.Ids = props.selected;
|
||||||
|
|
||||||
|
form.post(route("file.restore"));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="onClick()"
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:bg-emerald-600 hover:border-emerald-600 flex flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowPathIcon class="h-5 w-5" aria-hidden="true" /> Restore
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
@ -1,20 +1,33 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useForm } from "@inertiajs/vue3";
|
import { router, useForm } from "@inertiajs/vue3";
|
||||||
import TextInput from "../TextInput.vue";
|
import TextInput from "../TextInput.vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
const form = useForm({
|
const query = ref("");
|
||||||
search: "",
|
|
||||||
|
let parameters = "";
|
||||||
|
|
||||||
|
const onChange = () => {
|
||||||
|
parameters.set("search", query.value);
|
||||||
|
router.get(`${window.location.pathname}?${parameters.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
parameters = new URLSearchParams(window.location.search);
|
||||||
|
query.value = parameters.get("search");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form class="w-full h-[5rem] flex items-center">
|
<div class="w-full h-[5rem] flex items-center">
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
class="block w-full mr-2"
|
class="block w-full mr-2"
|
||||||
v-model="form.search"
|
v-model="query"
|
||||||
autocomplete
|
autocomplete
|
||||||
placeholder="Search for files or folders"
|
placeholder="Search for files or folders"
|
||||||
|
@keyup.enter.prevent="onChange()"
|
||||||
></TextInput>
|
></TextInput>
|
||||||
</form>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
39
resources/js/Components/custom/ShareButton.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script setup>
|
||||||
|
import { ShareIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import ShareFilesModal from "./ShareFilesModal.vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
// define props
|
||||||
|
const props = defineProps({
|
||||||
|
shareall: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shareFilesModalActive = ref(false);
|
||||||
|
|
||||||
|
const toggleShareFilesModal = (bool) => {
|
||||||
|
shareFilesModalActive.value = bool;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="toggleShareFilesModal(true)"
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:bg-teal-600 hover:border-teal-600 flex flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<ShareIcon class="h-5 w-5" aria-hidden="true" /> Share
|
||||||
|
</button>
|
||||||
|
<ShareFilesModal
|
||||||
|
v-model="shareFilesModalActive"
|
||||||
|
@close="toggleShareFilesModal(false)"
|
||||||
|
:shareall="shareall"
|
||||||
|
:selected="selected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
108
resources/js/Components/custom/ShareFilesModal.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick } from "vue";
|
||||||
|
import InputError from "../InputError.vue";
|
||||||
|
import InputLabel from "../InputLabel.vue";
|
||||||
|
import TextInput from "../TextInput.vue";
|
||||||
|
import Modal from "../Modal.vue";
|
||||||
|
import { useForm, usePage } from "@inertiajs/vue3";
|
||||||
|
|
||||||
|
const page = usePage();
|
||||||
|
|
||||||
|
// define props - drilled from ShareButton
|
||||||
|
const props = defineProps({
|
||||||
|
shareall: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
modelValue: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
// define refs
|
||||||
|
const emailInput = ref(null);
|
||||||
|
|
||||||
|
// pass "close" emit up towards NewDropdown.vue
|
||||||
|
// also clear form
|
||||||
|
const emit = defineEmits(["close"]);
|
||||||
|
const close = () => {
|
||||||
|
emit("close");
|
||||||
|
form.clearErrors();
|
||||||
|
form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
// create form
|
||||||
|
const form = useForm({
|
||||||
|
email: null,
|
||||||
|
all: false,
|
||||||
|
Ids: [],
|
||||||
|
parent_id: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// share function
|
||||||
|
const share = () => {
|
||||||
|
// define form fields
|
||||||
|
form.all = props.shareall;
|
||||||
|
form.Ids = props.selected;
|
||||||
|
form.parent_id = page.props.folder.id;
|
||||||
|
|
||||||
|
// post to file.share
|
||||||
|
form.post(route("file.share"), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
// close window when successful
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
// otherwise focus on email input field
|
||||||
|
onError: () => emailInput.value.focus,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusInput = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
emailInput.value.focus();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="props.modelValue" @close="close" @active="focusInput">
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
<h2 class="text-2xl">Share Files</h2>
|
||||||
|
<div class="mt-10">
|
||||||
|
<InputLabel
|
||||||
|
class="mb-2 text-lg"
|
||||||
|
for="email"
|
||||||
|
value="Recipient e-mail:"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
v-model="form.email"
|
||||||
|
class="block w-full mb-2"
|
||||||
|
:class="
|
||||||
|
form.errors.email
|
||||||
|
? 'border-red-800 focus:border-red-800 focus:ring-red-800'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@keyup.enter="share()"
|
||||||
|
ref="emailInput"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.emails" />
|
||||||
|
</div>
|
||||||
|
<div class="justify-center mt-5">
|
||||||
|
<button
|
||||||
|
@click="share()"
|
||||||
|
:disable="form.processing"
|
||||||
|
:class="{ 'opacity-25': form.processing }"
|
||||||
|
class="border border-sky-600 px-5 py-3 rounded hover:bg-emerald-700"
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
51
resources/js/Components/custom/UnshareButton.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup>
|
||||||
|
import { ShareIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useForm, usePage } from "@inertiajs/vue3";
|
||||||
|
|
||||||
|
const page = usePage();
|
||||||
|
|
||||||
|
// define props
|
||||||
|
const props = defineProps({
|
||||||
|
unshareall: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// create form
|
||||||
|
const form = useForm({
|
||||||
|
email: null,
|
||||||
|
all: false,
|
||||||
|
Ids: [],
|
||||||
|
parent_id: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// unshare function
|
||||||
|
const unshare = () => {
|
||||||
|
// define form fields
|
||||||
|
form.all = props.unshareall;
|
||||||
|
form.Ids = props.selected;
|
||||||
|
if (page.props.folder) {
|
||||||
|
form.parent_id = page.props.folder.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// post to file.share
|
||||||
|
form.post(route("file.unshare"), {
|
||||||
|
preserveScroll: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="unshare()"
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:bg-rose-600 hover:border-rose-600 flex flex justify-center items-center gap-2"
|
||||||
|
>
|
||||||
|
<ShareIcon class="h-5 w-5" aria-hidden="true" /> Unshare
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
@ -18,7 +18,7 @@ const upload = (event) => {
|
||||||
@change="upload"
|
@change="upload"
|
||||||
type="file"
|
type="file"
|
||||||
id="file"
|
id="file"
|
||||||
class="absolute left-0 top-0 bottom-0 right-0 opacity-0"
|
class="absolute left-0 top-2 bottom-0 right-0 opacity-0"
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,31 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { MenuItem } from "@headlessui/vue";
|
import { MenuItem } from "@headlessui/vue";
|
||||||
|
import { emitter } from "../../emitter.js";
|
||||||
|
|
||||||
|
// emit the upload event with Mitt
|
||||||
|
const upload = (event) => {
|
||||||
|
emitter.emit("FILE_UPLOAD_STARTED", event.target.files);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MenuItem v-slot="{ active }"
|
<MenuItem v-slot="{ active }"
|
||||||
><a
|
><a
|
||||||
href="#"
|
href="#"
|
||||||
class="block w-full items-center rounded-md py-2 border border-zinc-900 block w-full pl-3 pr-4 py-2 text-left text-base font-medium text-gray-600 dark:text-zinc-100 hover:border-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600transition duration-150 ease-in-out"
|
class="block w-full items-center rounded-md py-2 border border-zinc-900 block w-full pl-3 pr-4 py-2 text-left text-base font-medium text-gray-600 dark:text-zinc-100 hover:border-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600transition duration-150 ease-in-out relative"
|
||||||
>
|
>
|
||||||
Upload Folder
|
Upload Folder
|
||||||
<input
|
<input
|
||||||
@change="upload"
|
@change="upload"
|
||||||
type="file"
|
type="file"
|
||||||
id="file"
|
id="folder"
|
||||||
class="absolute left-0 top-0 bottom-0 right-0 opacity-0"
|
class="absolute left-0 top-2 bottom-0 right-0 opacity-0"
|
||||||
multiple
|
multiple
|
||||||
directory
|
directory
|
||||||
webkitdirectory
|
webkitdirectory
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
for="file"
|
for="folder"
|
||||||
class="opacity-0 absolute left-0 top-0 bottom-0 right-0 cursor-pointer"
|
class="opacity-0 absolute left-0 top-0 bottom-0 right-0 cursor-pointer"
|
||||||
></label>
|
></label>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import ResponsiveNavLink from "../ResponsiveNavLink.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Menu as="div" class="relative inline-block text-left">
|
<Menu as="div" class="relative inline-block text-left z-50">
|
||||||
<div>
|
<div>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
class="inline-flex w-full justify-center rounded-md bg-zinc-400/50 px-4 py-3 text-sm font-medium text-white hover:bg-opacity-30 hover:bg-zinc-400/20"
|
class="inline-flex w-full justify-center items-center rounded bg-zinc-400/50 border border-zinc-400/50 px-4 py-2 font-medium text-white hover:bg-opacity-30 hover:bg-zinc-400/20 hover:border-zinc-400/20"
|
||||||
>
|
>
|
||||||
{{ $page.props.auth.user.name }}
|
{{ $page.props.auth.user.name }}
|
||||||
<ChevronDownIcon class="ml-2 h-5 w-5" aria-hidden="true" />
|
<ChevronDownIcon class="ml-2 h-5 w-5" aria-hidden="true" />
|
||||||
|
|
@ -29,7 +29,7 @@ import ResponsiveNavLink from "../ResponsiveNavLink.vue";
|
||||||
<div class="px-1 py-1">
|
<div class="px-1 py-1">
|
||||||
<MenuItem v-slot="{ active }">
|
<MenuItem v-slot="{ active }">
|
||||||
<ResponsiveNavLink
|
<ResponsiveNavLink
|
||||||
:href="route('profile.edit')"
|
:href="route('profile')"
|
||||||
:class="[
|
:class="[
|
||||||
'group flex w-full items-center rounded-md py-2',
|
'group flex w-full items-center rounded-md py-2',
|
||||||
]"
|
]"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import Navigation from "@/Components/custom/Navigation.vue";
|
import Navigation from "@/Components/custom/Navigation.vue";
|
||||||
import SearchForm from "@/Components/custom/SearchForm.vue";
|
import SearchForm from "@/Components/custom/SearchForm.vue";
|
||||||
import UserDropdown from "@/Components/custom/UserDropdown.vue";
|
import UserDropdown from "@/Components/custom/UserDropdown.vue";
|
||||||
|
import FileUploadErrorModal from "@/Components/custom/FileUploadErrorModal.vue";
|
||||||
import { emitter } from "@/emitter.js";
|
import { emitter } from "@/emitter.js";
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { useForm, usePage } from "@inertiajs/vue3";
|
import { useForm, usePage } from "@inertiajs/vue3";
|
||||||
|
|
@ -10,10 +11,15 @@ import { useForm, usePage } from "@inertiajs/vue3";
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
// make refs
|
// make refs
|
||||||
const dragging = ref(false);
|
const dragging = ref(false);
|
||||||
|
const fileUploadError = ref(false);
|
||||||
|
const draggable = ref(false);
|
||||||
|
|
||||||
// add Mitt event listener on component load
|
// add Mitt event listener on component load
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("FILE_UPLOAD_STARTED", uploadFiles);
|
emitter.on("FILE_UPLOAD_STARTED", uploadFiles);
|
||||||
|
// determines whether this page is draggable
|
||||||
|
const regex = new RegExp("/files.*", "gi");
|
||||||
|
if (regex.test(page.url)) draggable.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// file drop function
|
// file drop function
|
||||||
|
|
@ -31,6 +37,7 @@ const onDrop = (event) => {
|
||||||
// create form input for file upload
|
// create form input for file upload
|
||||||
const fileUpload = useForm({
|
const fileUpload = useForm({
|
||||||
files: [],
|
files: [],
|
||||||
|
paths: [],
|
||||||
parent_id: null,
|
parent_id: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -39,9 +46,20 @@ const uploadFiles = (files) => {
|
||||||
// fill out form
|
// fill out form
|
||||||
fileUpload.files = files;
|
fileUpload.files = files;
|
||||||
fileUpload.parent_id = page.props.folder.id;
|
fileUpload.parent_id = page.props.folder.id;
|
||||||
|
fileUpload.paths = Array.from(files).map((file) => file.webkitRelativePath);
|
||||||
|
|
||||||
// send form to backend for processing
|
// send form to backend for processing
|
||||||
fileUpload.post(route("file.new"));
|
fileUpload.post(route("file.upload"), {
|
||||||
|
onError: (errors) => {
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
message = errors[Object.keys(errors)[0]];
|
||||||
|
fileUploadError.value = message;
|
||||||
|
} else {
|
||||||
|
message =
|
||||||
|
"Errors encountered while uploading file. Please try again.";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -49,6 +67,7 @@ const uploadFiles = (files) => {
|
||||||
<div class="h-screen w-full bg-zinc-900 text-gray-100 flex gap-4">
|
<div class="h-screen w-full bg-zinc-900 text-gray-100 flex gap-4">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main
|
<main
|
||||||
|
v-if="draggable"
|
||||||
@drop.prevent="onDrop"
|
@drop.prevent="onDrop"
|
||||||
@dragover.prevent="dragging = true"
|
@dragover.prevent="dragging = true"
|
||||||
@dragleave.prevent="dragging = false"
|
@dragleave.prevent="dragging = false"
|
||||||
|
|
@ -59,7 +78,7 @@ const uploadFiles = (files) => {
|
||||||
class="text-lg w-full h-full flex flex-col items-center justify-center border-2 border-dashed border-gray-700"
|
class="text-lg w-full h-full flex flex-col items-center justify-center border-2 border-dashed border-gray-700"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-1/5 h-1/5 border-2 rounded-lg border-gray-700 text-gray-300 text-6xl flex items-center justify-center"
|
class="w-40 h-40 border-2 rounded-lg border-gray-700 text-gray-300 text-6xl flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div class="arrow">+</div>
|
<div class="arrow">+</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,9 +90,32 @@ const uploadFiles = (files) => {
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div class="h-4 bg-zinc-900" v-if="fileUpload.progress">
|
||||||
|
<div
|
||||||
|
class="h-full bg-sky-600 transition-all"
|
||||||
|
:style="{ width: `${form.progress.percentage}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
<main v-else class="flex flex-col flex-1 px-4 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<SearchForm />
|
||||||
|
<UserDropdown />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<FileUploadErrorModal v-if="fileUploadError" :message="fileUploadError">
|
||||||
|
<button
|
||||||
|
@click="fileUploadError = false"
|
||||||
|
class="border border-sky-600 px-5 py-3 rounded hover:bg-sky-600"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</FileUploadErrorModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import GuestLayout from "@/Layouts/GuestLayout.vue";
|
||||||
import InputError from "@/Components/InputError.vue";
|
import InputError from "@/Components/InputError.vue";
|
||||||
import InputLabel from "@/Components/InputLabel.vue";
|
import InputLabel from "@/Components/InputLabel.vue";
|
||||||
import TextInput from "@/Components/TextInput.vue";
|
import TextInput from "@/Components/TextInput.vue";
|
||||||
import { Head, Link, useForm } from "@inertiajs/vue3";
|
import { Head, Link, useForm, router } from "@inertiajs/vue3";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
canResetPassword: {
|
canResetPassword: {
|
||||||
|
|
@ -21,9 +21,11 @@ const form = useForm({
|
||||||
remember: false,
|
remember: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// if log in is a success, reroute to user files
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
form.post(route("login"), {
|
form.post(route("login"), {
|
||||||
onFinish: () => form.reset("password"),
|
onFinish: () => form.reset("password"),
|
||||||
|
onSuccess: () => router.visit(route("/files")),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -79,11 +81,10 @@ const submit = () => {
|
||||||
|
|
||||||
<div class="flex items-center justify-end mt-4 gap-6">
|
<div class="flex items-center justify-end mt-4 gap-6">
|
||||||
<Link
|
<Link
|
||||||
v-if="canResetPassword"
|
:href="route('register')"
|
||||||
:href="route('password.request')"
|
|
||||||
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md"
|
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md"
|
||||||
>
|
>
|
||||||
Forgot your password?
|
Don't have an account?
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import GuestLayout from "@/Layouts/GuestLayout.vue";
|
||||||
import InputError from "@/Components/InputError.vue";
|
import InputError from "@/Components/InputError.vue";
|
||||||
import InputLabel from "@/Components/InputLabel.vue";
|
import InputLabel from "@/Components/InputLabel.vue";
|
||||||
import TextInput from "@/Components/TextInput.vue";
|
import TextInput from "@/Components/TextInput.vue";
|
||||||
import { Head, Link, useForm } from "@inertiajs/vue3";
|
import { Head, Link, useForm, router } from "@inertiajs/vue3";
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
name: "",
|
name: "",
|
||||||
|
|
@ -15,6 +15,12 @@ const form = useForm({
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
form.post(route("register"), {
|
form.post(route("register"), {
|
||||||
onFinish: () => form.reset("password", "password_confirmation"),
|
onFinish: () => form.reset("password", "password_confirmation"),
|
||||||
|
onSuccess: () => {
|
||||||
|
alert(
|
||||||
|
"Registration successful. Please check your e-mails for login link."
|
||||||
|
);
|
||||||
|
router.visit(route("/login"));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
|
|
||||||
import { Head } from "@inertiajs/vue3";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AuthenticatedLayout>
|
|
||||||
<template #header>
|
|
||||||
<h2
|
|
||||||
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="py-12">
|
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
||||||
<div
|
|
||||||
class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg"
|
|
||||||
>
|
|
||||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
|
||||||
You're logged in!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AuthenticatedLayout>
|
|
||||||
</template>
|
|
||||||
221
resources/js/Pages/RecycleBin.vue
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
<script setup>
|
||||||
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
|
||||||
|
import { router, Link } from "@inertiajs/vue3";
|
||||||
|
import FileIcon from "@/Components/custom/FileIcon.vue";
|
||||||
|
import Checkbox from "@/Components/Checkbox.vue";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import RestoreButton from "@/Components/custom/RestoreButton.vue";
|
||||||
|
import DeleteButton from "@/Components/custom/DeleteButton.vue";
|
||||||
|
import { faRecycle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import MainBreadcrumbButton from "@/Components/custom/MainBreadcrumbButton.vue";
|
||||||
|
|
||||||
|
// defines refs
|
||||||
|
const allSelected = ref(false);
|
||||||
|
const fileSelectedStatus = ref({});
|
||||||
|
|
||||||
|
// defines props
|
||||||
|
const { files } = defineProps({
|
||||||
|
files: Object,
|
||||||
|
folder: Object,
|
||||||
|
ancestors: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
// defines computed properties
|
||||||
|
// gets a formatted array of all currently-selected files
|
||||||
|
const currentlySelected = computed(() => {
|
||||||
|
// casts the ref object of currently selected files to array
|
||||||
|
// { id: bool } becomes [id, bool]
|
||||||
|
let array = Object.entries(fileSelectedStatus.value);
|
||||||
|
// filters array for all the id's in [id, bool] where bool === true
|
||||||
|
array = array.filter((idBool) => idBool[1]);
|
||||||
|
// maps filtered array from [id, bool] to [id]
|
||||||
|
array = array.map((idBool) => idBool[0]);
|
||||||
|
// returns
|
||||||
|
return array;
|
||||||
|
});
|
||||||
|
|
||||||
|
// selects all files
|
||||||
|
const selectAll = () => {
|
||||||
|
files.data.forEach((file) => {
|
||||||
|
// set each fileSelectedStatus value to the value of allSelected
|
||||||
|
fileSelectedStatus.value[file.id] = allSelected.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// selects file
|
||||||
|
const selectFile = (file) => {
|
||||||
|
// get current value
|
||||||
|
const currentValue = fileSelectedStatus.value[file.id];
|
||||||
|
// invert
|
||||||
|
const newValue = !currentValue;
|
||||||
|
// set file's value
|
||||||
|
fileSelectedStatus.value[file.id] = newValue;
|
||||||
|
|
||||||
|
// if new value is negative, disable allSelected
|
||||||
|
if (!newValue) {
|
||||||
|
allSelected.value = false;
|
||||||
|
} else {
|
||||||
|
// else check if this "completes" the selection of all files in folder
|
||||||
|
let isallSelected = true;
|
||||||
|
|
||||||
|
files.data.forEach((entry) => {
|
||||||
|
// get current value at id
|
||||||
|
const status = fileSelectedStatus.value[entry.id];
|
||||||
|
// if the fileSelectedStatus value at id is falsy, set isallSelected to false
|
||||||
|
if (!status) isallSelected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// update allSelected value
|
||||||
|
allSelected.value = isallSelected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// opens folder when clicked
|
||||||
|
const openFolder = (file) => {
|
||||||
|
if (!file.is_folder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.visit(
|
||||||
|
route("recycleBin", {
|
||||||
|
folder: file.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticatedLayout>
|
||||||
|
<nav class="flex items-center justify-between mb-2">
|
||||||
|
<ol class="inline-flex items-center">
|
||||||
|
<MainBreadcrumbButton
|
||||||
|
:href="route('recycleBin')"
|
||||||
|
:active="$page.url === '/recycle-bin'"
|
||||||
|
class="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="faRecycle"
|
||||||
|
class="h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</MainBreadcrumbButton>
|
||||||
|
<li
|
||||||
|
v-if="ancestors"
|
||||||
|
v-for="(ancestor, index) of ancestors.data"
|
||||||
|
:key="ancestor.id"
|
||||||
|
class="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center" v-if="ancestor.parent_id">
|
||||||
|
<div class="mx-2">➤</div>
|
||||||
|
<Link
|
||||||
|
v-if="index == ancestors.data.length - 1"
|
||||||
|
class="border-sky-600 border px-3 py-2 rounded bg-sky-600"
|
||||||
|
:href="route('sharedBy', { folder: ancestor.id })"
|
||||||
|
>
|
||||||
|
{{ ancestor.name }}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
v-else
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:border-sky-600"
|
||||||
|
:href="route('sharedBy', { folder: ancestor.id })"
|
||||||
|
>
|
||||||
|
{{ ancestor.name }}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<RestoreButton
|
||||||
|
:restoreall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
<DeleteButton
|
||||||
|
:getall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="overflow-auto h-full w-full" v-if="files.data.length">
|
||||||
|
<table class="w-full border-separate">
|
||||||
|
<colgroup>
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr class="sticky top-0 z-20">
|
||||||
|
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
||||||
|
<Checkbox
|
||||||
|
v-model:checked="allSelected"
|
||||||
|
@change="selectAll()"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Recycled
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="file of files.data"
|
||||||
|
:key="file.id"
|
||||||
|
@dblclick="openFolder(file)"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 w-12"
|
||||||
|
@click="selectFile(file)"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:checked="fileSelectedStatus[file.id]"
|
||||||
|
:v-model="
|
||||||
|
fileSelectedStatus[file.id] || allSelected
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 flex items-center justify-center relative"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 bg-zinc-900 w-11 h-10 pl-1"
|
||||||
|
>
|
||||||
|
<FileIcon :file="file" />
|
||||||
|
</span>
|
||||||
|
<span class="pl-10 pr-10 overflow-auto">
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.deleted_at }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.size }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="h-full" v-else>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center h-full text-3xl"
|
||||||
|
>
|
||||||
|
This folder is empty
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
</template>
|
||||||
258
resources/js/Pages/SharedBy.vue
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { router, Link } from "@inertiajs/vue3";
|
||||||
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
|
||||||
|
import FileIcon from "@/Components/custom/FileIcon.vue";
|
||||||
|
import Checkbox from "@/Components/Checkbox.vue";
|
||||||
|
import DownloadButton from "@/Components/custom/DownloadButton.vue";
|
||||||
|
import MainBreadcrumbButton from "@/Components/custom/MainBreadcrumbButton.vue";
|
||||||
|
import UnshareButton from "@/Components/custom/UnshareButton.vue";
|
||||||
|
|
||||||
|
// defines refs
|
||||||
|
const allSelected = ref(false);
|
||||||
|
const fileSelectedStatus = ref({});
|
||||||
|
|
||||||
|
// defines props
|
||||||
|
const { files } = defineProps({
|
||||||
|
files: Object,
|
||||||
|
folder: Object,
|
||||||
|
ancestors: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
// defines computed properties
|
||||||
|
// gets a formatted array of all currently-selected files
|
||||||
|
const currentlySelected = computed(() => {
|
||||||
|
// casts the ref object of currently selected files to array
|
||||||
|
// { id: bool } becomes [id, bool]
|
||||||
|
let array = Object.entries(fileSelectedStatus.value);
|
||||||
|
// filters array for all the id's in [id, bool] where bool === true
|
||||||
|
array = array.filter((idBool) => idBool[1]);
|
||||||
|
// maps filtered array from [id, bool] to [id]
|
||||||
|
array = array.map((idBool) => idBool[0]);
|
||||||
|
// returns
|
||||||
|
return array;
|
||||||
|
});
|
||||||
|
|
||||||
|
// opens folder when clicked
|
||||||
|
const openFile = (file) => {
|
||||||
|
// if the file is a regular file, download on double click
|
||||||
|
if (!file.is_folder) {
|
||||||
|
// create a URL parameter string based on conditions
|
||||||
|
const parameters = new URLSearchParams();
|
||||||
|
// append value of all
|
||||||
|
parameters.append("all", false);
|
||||||
|
// append file
|
||||||
|
parameters.append("Ids[]", file.id);
|
||||||
|
|
||||||
|
// define opts
|
||||||
|
const opts = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
// fetch download link
|
||||||
|
fetch(route("file.downloadShared") + `?${parameters.toString()}`, opts)
|
||||||
|
// then get response
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
// then get the json
|
||||||
|
.then((json) => {
|
||||||
|
// if response contains no link, return - shouldn't be happening
|
||||||
|
if (!json.url) return;
|
||||||
|
// else create a link to the document for download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = json.url;
|
||||||
|
link.download = json.filename;
|
||||||
|
// then access it
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// else visit folder
|
||||||
|
router.visit(
|
||||||
|
route("sharedBy", {
|
||||||
|
folder: file.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// selects all files
|
||||||
|
const selectAll = () => {
|
||||||
|
files.data.forEach((file) => {
|
||||||
|
// set each fileSelectedStatus value to the value of allSelected
|
||||||
|
fileSelectedStatus.value[file.id] = allSelected.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// selects file
|
||||||
|
const selectFile = (file) => {
|
||||||
|
// get current value
|
||||||
|
const currentValue = fileSelectedStatus.value[file.id];
|
||||||
|
// invert
|
||||||
|
const newValue = !currentValue;
|
||||||
|
// set file's value
|
||||||
|
fileSelectedStatus.value[file.id] = newValue;
|
||||||
|
|
||||||
|
// if new value is negative, disable allSelected
|
||||||
|
if (!newValue) {
|
||||||
|
allSelected.value = false;
|
||||||
|
} else {
|
||||||
|
// else check if this "completes" the selection of all files in folder
|
||||||
|
let isallSelected = true;
|
||||||
|
|
||||||
|
files.data.forEach((entry) => {
|
||||||
|
// get current value at id
|
||||||
|
const status = fileSelectedStatus.value[entry.id];
|
||||||
|
// if the fileSelectedStatus value at id is falsy, set isallSelected to false
|
||||||
|
if (!status) isallSelected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// update allSelected value
|
||||||
|
allSelected.value = isallSelected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticatedLayout>
|
||||||
|
<nav class="flex items-center justify-between mb-2">
|
||||||
|
<ol class="inline-flex items-center">
|
||||||
|
<MainBreadcrumbButton
|
||||||
|
:href="route('sharedBy')"
|
||||||
|
:active="$page.url === '/shared-by-me'"
|
||||||
|
>
|
||||||
|
Shared by Me
|
||||||
|
</MainBreadcrumbButton>
|
||||||
|
<li
|
||||||
|
v-if="ancestors"
|
||||||
|
v-for="(ancestor, index) of ancestors.data"
|
||||||
|
:key="ancestor.id"
|
||||||
|
class="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center" v-if="ancestor.parent_id">
|
||||||
|
<div class="mx-2">➤</div>
|
||||||
|
<Link
|
||||||
|
v-if="index == ancestors.data.length - 1"
|
||||||
|
class="border-sky-600 border px-3 py-2 rounded bg-sky-600"
|
||||||
|
:href="route('sharedBy', { folder: ancestor.id })"
|
||||||
|
>
|
||||||
|
{{ ancestor.name }}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
v-else
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:border-sky-600"
|
||||||
|
:href="route('sharedBy', { folder: ancestor.id })"
|
||||||
|
>
|
||||||
|
{{ ancestor.name }}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<UnshareButton
|
||||||
|
:unshareall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
<DownloadButton
|
||||||
|
:getall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="overflow-auto h-full w-full" v-if="files.data.length">
|
||||||
|
<table class="w-full border-separate">
|
||||||
|
<colgroup>
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr class="sticky top-0 z-20">
|
||||||
|
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
||||||
|
<Checkbox
|
||||||
|
v-model:checked="allSelected"
|
||||||
|
@change="selectAll()"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Modified
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Shared With
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="file of files.data"
|
||||||
|
:key="file.id"
|
||||||
|
@dblclick="openFile(file)"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 w-12"
|
||||||
|
@click="selectFile(file)"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:checked="fileSelectedStatus[file.id]"
|
||||||
|
:v-model="
|
||||||
|
fileSelectedStatus[file.id] || allSelected
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 flex items-center justify-center relative"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 bg-zinc-900 w-11 h-10 pl-1"
|
||||||
|
>
|
||||||
|
<FileIcon :file="file" />
|
||||||
|
</span>
|
||||||
|
<span class="pl-10 pr-10">
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.updated_at }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.shared_with }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.size }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="h-full" v-else>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center h-full text-3xl"
|
||||||
|
>
|
||||||
|
This folder is empty
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
</template>
|
||||||
256
resources/js/Pages/SharedWith.vue
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { router, Link } from "@inertiajs/vue3";
|
||||||
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
|
||||||
|
import FileIcon from "@/Components/custom/FileIcon.vue";
|
||||||
|
import Checkbox from "@/Components/Checkbox.vue";
|
||||||
|
import DownloadButton from "@/Components/custom/DownloadButton.vue";
|
||||||
|
import MainBreadcrumbButton from "@/Components/custom/MainBreadcrumbButton.vue";
|
||||||
|
|
||||||
|
// defines refs
|
||||||
|
const allSelected = ref(false);
|
||||||
|
const fileSelectedStatus = ref({});
|
||||||
|
|
||||||
|
// defines props
|
||||||
|
const { files } = defineProps({
|
||||||
|
files: Object,
|
||||||
|
folder: Object,
|
||||||
|
ancestors: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
// defines computed properties
|
||||||
|
// gets a formatted array of all currently-selected files
|
||||||
|
const currentlySelected = computed(() => {
|
||||||
|
// casts the ref object of currently selected files to array
|
||||||
|
// { id: bool } becomes [id, bool]
|
||||||
|
let array = Object.entries(fileSelectedStatus.value);
|
||||||
|
// filters array for all the id's in [id, bool] where bool === true
|
||||||
|
array = array.filter((idBool) => idBool[1]);
|
||||||
|
// maps filtered array from [id, bool] to [id]
|
||||||
|
array = array.map((idBool) => idBool[0]);
|
||||||
|
// returns
|
||||||
|
return array;
|
||||||
|
});
|
||||||
|
|
||||||
|
// opens folder when clicked
|
||||||
|
const openFile = (file) => {
|
||||||
|
// if the file is a regular file, download on double click
|
||||||
|
if (!file.is_folder) {
|
||||||
|
// create a URL parameter string based on conditions
|
||||||
|
const parameters = new URLSearchParams();
|
||||||
|
// append value of all
|
||||||
|
parameters.append("all", false);
|
||||||
|
// append file
|
||||||
|
parameters.append("Ids[]", file.id);
|
||||||
|
// then append parent_id (current directory) (if one exists)
|
||||||
|
if (page.props.folder.id)
|
||||||
|
parameters.append("parent_id", page.props.folder.id);
|
||||||
|
|
||||||
|
// define opts
|
||||||
|
const opts = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
// fetch download link
|
||||||
|
fetch(route("file.downloadShared") + `?${parameters.toString()}`, opts)
|
||||||
|
// then get response
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
// then get the json
|
||||||
|
.then((json) => {
|
||||||
|
// if response contains no link, return - shouldn't be happening
|
||||||
|
if (!json.url) return;
|
||||||
|
// else create a link to the document for download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = json.url;
|
||||||
|
link.download = json.filename;
|
||||||
|
// then access it
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// else visit folder
|
||||||
|
router.visit(
|
||||||
|
route("sharedWith", {
|
||||||
|
folder: file.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// selects all files
|
||||||
|
const selectAll = () => {
|
||||||
|
files.data.forEach((file) => {
|
||||||
|
// set each fileSelectedStatus value to the value of allSelected
|
||||||
|
fileSelectedStatus.value[file.id] = allSelected.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// selects file
|
||||||
|
const selectFile = (file) => {
|
||||||
|
// get current value
|
||||||
|
const currentValue = fileSelectedStatus.value[file.id];
|
||||||
|
// invert
|
||||||
|
const newValue = !currentValue;
|
||||||
|
// set file's value
|
||||||
|
fileSelectedStatus.value[file.id] = newValue;
|
||||||
|
|
||||||
|
// if new value is negative, disable allSelected
|
||||||
|
if (!newValue) {
|
||||||
|
allSelected.value = false;
|
||||||
|
} else {
|
||||||
|
// else check if this "completes" the selection of all files in folder
|
||||||
|
let isallSelected = true;
|
||||||
|
|
||||||
|
files.data.forEach((entry) => {
|
||||||
|
// get current value at id
|
||||||
|
const status = fileSelectedStatus.value[entry.id];
|
||||||
|
// if the fileSelectedStatus value at id is falsy, set isallSelected to false
|
||||||
|
if (!status) isallSelected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// update allSelected value
|
||||||
|
allSelected.value = isallSelected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticatedLayout>
|
||||||
|
<nav class="flex items-center justify-between mb-2">
|
||||||
|
<ol class="inline-flex items-center">
|
||||||
|
<MainBreadcrumbButton
|
||||||
|
:href="route('sharedWith')"
|
||||||
|
:active="$page.url === '/shared-with-me'"
|
||||||
|
>
|
||||||
|
Shared with Me
|
||||||
|
</MainBreadcrumbButton>
|
||||||
|
<li
|
||||||
|
v-if="ancestors"
|
||||||
|
v-for="(ancestor, index) of ancestors.data"
|
||||||
|
:key="ancestor.id"
|
||||||
|
class="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center" v-if="ancestor.parent_id">
|
||||||
|
<div class="mx-2">➤</div>
|
||||||
|
<Link
|
||||||
|
v-if="index == ancestors.data.length - 1"
|
||||||
|
class="border-sky-600 border px-3 py-2 rounded bg-sky-600"
|
||||||
|
:href="route('sharedWith', { folder: ancestor.id })"
|
||||||
|
>
|
||||||
|
{{ ancestor.name }}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
v-else
|
||||||
|
class="border-gray-700 border px-3 py-2 rounded hover:border-sky-600"
|
||||||
|
:href="route('sharedWith', { folder: ancestor.id })"
|
||||||
|
>
|
||||||
|
{{ ancestor.name }}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<DownloadButton
|
||||||
|
:getall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="overflow-auto h-full w-full" v-if="files.data.length">
|
||||||
|
<table class="w-full border-separate">
|
||||||
|
<colgroup>
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr class="sticky top-0 z-20">
|
||||||
|
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
||||||
|
<Checkbox
|
||||||
|
v-model:checked="allSelected"
|
||||||
|
@change="selectAll()"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Modified
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Owner
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="file of files.data"
|
||||||
|
:key="file.id"
|
||||||
|
@dblclick="openFile(file)"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 w-12"
|
||||||
|
@click="selectFile(file)"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:checked="fileSelectedStatus[file.id]"
|
||||||
|
:v-model="
|
||||||
|
fileSelectedStatus[file.id] || allSelected
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 flex items-center justify-center relative"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 bg-zinc-900 w-11 h-10 pl-1"
|
||||||
|
>
|
||||||
|
<FileIcon :file="file" />
|
||||||
|
</span>
|
||||||
|
<span class="pl-10 pr-10">
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.updated_at }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.owner }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.size }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="h-full" v-else>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center h-full text-3xl"
|
||||||
|
>
|
||||||
|
This folder is empty
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
</template>
|
||||||
|
|
@ -1,34 +1,135 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
|
||||||
import { router, Link } from "@inertiajs/vue3";
|
import { router, Link, usePage } from "@inertiajs/vue3";
|
||||||
import { HomeIcon } from "@heroicons/vue/24/outline";
|
import { HomeIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import FileIcon from "@/Components/custom/FileIcon.vue";
|
||||||
|
import Checkbox from "@/Components/Checkbox.vue";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import RecycleButton from "@/Components/custom/RecycleButton.vue";
|
||||||
|
import DownloadButton from "@/Components/custom/DownloadButton.vue";
|
||||||
|
import ShareButton from "@/Components/custom/ShareButton.vue";
|
||||||
|
import MainBreadcrumbButton from "@/Components/custom/MainBreadcrumbButton.vue";
|
||||||
|
|
||||||
const { files, folder } = defineProps({
|
const page = usePage();
|
||||||
|
|
||||||
|
// defines refs
|
||||||
|
const allSelected = ref(false);
|
||||||
|
const fileSelectedStatus = ref({});
|
||||||
|
|
||||||
|
// defines props
|
||||||
|
const { files } = defineProps({
|
||||||
files: Object,
|
files: Object,
|
||||||
folder: Object,
|
folder: Object,
|
||||||
ancestors: Array,
|
ancestors: Array,
|
||||||
});
|
});
|
||||||
|
|
||||||
const openFolder = (file) => {
|
// defines computed properties
|
||||||
if (!file.is_folder) return;
|
// gets a formatted array of all currently-selected files
|
||||||
|
const currentlySelected = computed(() => {
|
||||||
|
// casts the ref object of currently selected files to array
|
||||||
|
// { id: bool } becomes [id, bool]
|
||||||
|
let array = Object.entries(fileSelectedStatus.value);
|
||||||
|
// filters array for all the id's in [id, bool] where bool === true
|
||||||
|
array = array.filter((idBool) => idBool[1]);
|
||||||
|
// maps filtered array from [id, bool] to [id]
|
||||||
|
array = array.map((idBool) => idBool[0]);
|
||||||
|
// returns
|
||||||
|
return array;
|
||||||
|
});
|
||||||
|
|
||||||
|
// opens folder when clicked
|
||||||
|
const openFile = (file) => {
|
||||||
|
if (!file.is_folder) {
|
||||||
|
// create a URL parameter string based on conditions
|
||||||
|
const parameters = new URLSearchParams();
|
||||||
|
// append value of all
|
||||||
|
parameters.append("all", false);
|
||||||
|
// append selected Id's
|
||||||
|
parameters.append("Ids[]", file.id);
|
||||||
|
// then append parent_id (current directory)
|
||||||
|
parameters.append("parent_id", page.props.folder.id);
|
||||||
|
|
||||||
|
// define opts
|
||||||
|
const opts = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
// fetch download link
|
||||||
|
fetch(route("file.download") + `?${parameters.toString()}`, opts)
|
||||||
|
// then get response
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
// then get the json
|
||||||
|
.then((json) => {
|
||||||
|
// if response contains no link, return - shouldn't be happening
|
||||||
|
if (!json.url) return;
|
||||||
|
// else create a link to the document for download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = json.url;
|
||||||
|
link.download = json.filename;
|
||||||
|
// then access it
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
router.visit(
|
router.visit(
|
||||||
route("userFiles", {
|
route("userFiles", {
|
||||||
folder: file.path,
|
folder: file.path,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// selects all files
|
||||||
|
const selectAll = () => {
|
||||||
|
files.data.forEach((file) => {
|
||||||
|
// set each fileSelectedStatus value to the value of allSelected
|
||||||
|
fileSelectedStatus.value[file.id] = allSelected.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// selects file
|
||||||
|
const selectFile = (file) => {
|
||||||
|
// get current value
|
||||||
|
const currentValue = fileSelectedStatus.value[file.id];
|
||||||
|
// invert
|
||||||
|
const newValue = !currentValue;
|
||||||
|
// set file's value
|
||||||
|
fileSelectedStatus.value[file.id] = newValue;
|
||||||
|
|
||||||
|
// if new value is negative, disable allSelected
|
||||||
|
if (!newValue) {
|
||||||
|
allSelected.value = false;
|
||||||
|
} else {
|
||||||
|
// else check if this "completes" the selection of all files in folder
|
||||||
|
let isallSelected = true;
|
||||||
|
|
||||||
|
files.data.forEach((entry) => {
|
||||||
|
// get current value at id
|
||||||
|
const status = fileSelectedStatus.value[entry.id];
|
||||||
|
// if the fileSelectedStatus value at id is falsy, set isallSelected to false
|
||||||
|
if (!status) isallSelected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// update allSelected value
|
||||||
|
allSelected.value = isallSelected;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Head title="Welcome" />
|
||||||
<AuthenticatedLayout>
|
<AuthenticatedLayout>
|
||||||
<nav class="flex items-center justify-between mb-2">
|
<nav class="flex items-center justify-between mb-2">
|
||||||
<ol class="inline-flex items-center">
|
<ol class="inline-flex items-center">
|
||||||
<Link
|
<MainBreadcrumbButton
|
||||||
class="border-gray-700 border px-3 py-2 rounded hover:border-sky-600"
|
|
||||||
:href="route('userFiles')"
|
:href="route('userFiles')"
|
||||||
|
:active="$page.url === '/files'"
|
||||||
>
|
>
|
||||||
<HomeIcon class="h-5 w-5" aria-hidden="true" />
|
<HomeIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
</Link>
|
</MainBreadcrumbButton>
|
||||||
<li
|
<li
|
||||||
v-for="(ancestor, index) of ancestors.data"
|
v-for="(ancestor, index) of ancestors.data"
|
||||||
:key="ancestor.id"
|
:key="ancestor.id"
|
||||||
|
|
@ -57,66 +158,118 @@ const openFolder = (file) => {
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<ShareButton
|
||||||
|
:shareall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
<DownloadButton
|
||||||
|
:getall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
<RecycleButton
|
||||||
|
:wipeall="allSelected"
|
||||||
|
:selected="currentlySelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<table v-if="files.data.length" class="w-full border-separate">
|
<div class="overflow-auto h-full w-full" v-if="files.data.length">
|
||||||
<colgroup>
|
<table class="w-full border-separate">
|
||||||
<col />
|
<colgroup>
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
</colgroup>
|
<col />
|
||||||
<thead>
|
<col />
|
||||||
<tr>
|
</colgroup>
|
||||||
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
<thead>
|
||||||
Name
|
<tr class="sticky top-0 z-20">
|
||||||
</th>
|
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
||||||
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
<Checkbox
|
||||||
Modified
|
v-model:checked="allSelected"
|
||||||
</th>
|
@change="selectAll()"
|
||||||
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
/>
|
||||||
Owner
|
</th>
|
||||||
</th>
|
<th
|
||||||
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
Size
|
>
|
||||||
</th>
|
Name
|
||||||
</tr>
|
</th>
|
||||||
</thead>
|
<th
|
||||||
<tbody>
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
<tr
|
>
|
||||||
v-for="file of files.data"
|
Modified
|
||||||
:key="file.id"
|
</th>
|
||||||
@click="openFolder(file)"
|
<th
|
||||||
>
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
<td
|
>
|
||||||
class="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
Owner
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="file of files.data"
|
||||||
|
:key="file.id"
|
||||||
|
@dblclick="openFile(file)"
|
||||||
>
|
>
|
||||||
{{ file.name }}
|
<td
|
||||||
</td>
|
class="text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 w-12"
|
||||||
<td
|
@click="selectFile(file)"
|
||||||
class="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
>
|
||||||
>
|
<Checkbox
|
||||||
{{ file.updated_at }}
|
:checked="fileSelectedStatus[file.id]"
|
||||||
</td>
|
:v-model="
|
||||||
<td
|
fileSelectedStatus[file.id] || allSelected
|
||||||
class="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
"
|
||||||
>
|
/>
|
||||||
{{ file.owner /* get name */ }}
|
</td>
|
||||||
</td>
|
<td
|
||||||
<td
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 flex items-center justify-center relative"
|
||||||
class="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
>
|
||||||
>
|
<span
|
||||||
{{ file.size == null ? 0 : file.size }}
|
class="absolute left-0 bg-zinc-900 w-11 h-10 pl-1"
|
||||||
</td>
|
>
|
||||||
</tr>
|
<FileIcon :file="file" />
|
||||||
</tbody>
|
</span>
|
||||||
</table>
|
<span class="pl-10 pr-10 max-w-md overflow-auto">
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.updated_at }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.owner }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||||
|
>
|
||||||
|
{{ file.size }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
<div class="h-full" v-else>
|
<div class="h-full" v-else>
|
||||||
<div class="text-lg mt-1.5">
|
<div class="text-lg mt-1.5">
|
||||||
<span class="arrow">🡄</span> click here
|
<span class="arrow">🡄</span> click here
|
||||||
to upload your file
|
to upload your files or drag and drop below
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center h-full text-3xl">
|
<div
|
||||||
|
class="flex flex-col items-center justify-center h-full text-3xl"
|
||||||
|
>
|
||||||
This folder is empty
|
This folder is empty
|
||||||
|
<div class="text-base">drop your files here</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AuthenticatedLayout>
|
</AuthenticatedLayout>
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import { Head, Link } from "@inertiajs/vue3";
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
canLogin: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
canRegister: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
laravelVersion: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
phpVersion: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Head title="Welcome" />
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="canLogin"
|
|
||||||
class="sm:fixed sm:top-0 sm:right-0 p-6 text-right"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
v-if="$page.props.auth.user"
|
|
||||||
:href="route('dashboard')"
|
|
||||||
class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
|
|
||||||
>Dashboard</Link
|
|
||||||
>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<Link
|
|
||||||
:href="route('login')"
|
|
||||||
class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
|
|
||||||
>Log in</Link
|
|
||||||
>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
v-if="canRegister"
|
|
||||||
:href="route('register')"
|
|
||||||
class="ml-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
|
|
||||||
>Register</Link
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,23 +1,29 @@
|
||||||
import './bootstrap';
|
import "./bootstrap";
|
||||||
import '../css/app.css';
|
import "../css/app.css";
|
||||||
|
|
||||||
import { createApp, h } from 'vue';
|
import { createApp, h } from "vue";
|
||||||
import { createInertiaApp } from '@inertiajs/vue3';
|
import { createInertiaApp } from "@inertiajs/vue3";
|
||||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
|
||||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
|
import { ZiggyVue } from "../../vendor/tightenco/ziggy/dist/vue.m";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
|
||||||
|
|
||||||
createInertiaApp({
|
createInertiaApp({
|
||||||
title: (title) => `${title} - ${appName}`,
|
title: (title) => `${title} - ${appName}`,
|
||||||
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
|
resolve: (name) =>
|
||||||
|
resolvePageComponent(
|
||||||
|
`./Pages/${name}.vue`,
|
||||||
|
import.meta.glob("./Pages/**/*.vue")
|
||||||
|
),
|
||||||
setup({ el, App, props, plugin }) {
|
setup({ el, App, props, plugin }) {
|
||||||
return createApp({ render: () => h(App, props) })
|
return createApp({ render: () => h(App, props) })
|
||||||
.use(plugin)
|
.use(plugin)
|
||||||
.use(ZiggyVue, Ziggy)
|
.use(ZiggyVue, Ziggy)
|
||||||
|
.component("font-awesome-icon", FontAwesomeIcon)
|
||||||
.mount(el);
|
.mount(el);
|
||||||
},
|
},
|
||||||
progress: {
|
progress: {
|
||||||
color: '#4B5563',
|
color: "#4B5563",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
85
resources/js/getMimeType.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
export const isImage = (file) => {
|
||||||
|
// looks for "image/whatever" in mime type
|
||||||
|
const regex = new RegExp("^image/.+", "gi");
|
||||||
|
// returns boolean
|
||||||
|
return regex.test(file.mime);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAudio = (file) => {
|
||||||
|
// looks for "audio/whatever" in mime type
|
||||||
|
const regex = new RegExp("^audio/.+", "gi");
|
||||||
|
// returns boolean
|
||||||
|
return regex.test(file.mime);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isVideo = (file) => {
|
||||||
|
// looks for "video/whatever" in mime type
|
||||||
|
const regex = new RegExp("^video/.+", "gi");
|
||||||
|
// returns boolean
|
||||||
|
return regex.test(file.mime);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isText = (file) => {
|
||||||
|
// looks for "text/whatever" in mime type
|
||||||
|
const regex = new RegExp("^text/.+", "gi");
|
||||||
|
// checks that file.mime is neither text/pdf or text/x-pdf
|
||||||
|
if (file.mime == "text/pdf" || file.mime == "text/x-pdf") return false;
|
||||||
|
// returns boolean
|
||||||
|
return regex.test(file.mime);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPdf = (file) => {
|
||||||
|
// matches against known pdf formats
|
||||||
|
const formats = [
|
||||||
|
"text/pdf",
|
||||||
|
"text/x-pdf",
|
||||||
|
"application/pdf",
|
||||||
|
"application/x-pdf",
|
||||||
|
"application/vnd.pdf",
|
||||||
|
"application/acrobat",
|
||||||
|
];
|
||||||
|
|
||||||
|
return formats.includes(file.mime);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDoc = (file) => {
|
||||||
|
// matches against known word/doc formats
|
||||||
|
const formats = [
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.ms-word.document.macroEnabled12",
|
||||||
|
"application/vnd.ms-word.template.macroEnabled.12",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.oasis.opendocument.text",
|
||||||
|
"application/x-abiword",
|
||||||
|
];
|
||||||
|
|
||||||
|
return formats.includes(file.mime);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSpreadsheet = (file) => {
|
||||||
|
// matches against known excel/spreadsheet formats
|
||||||
|
const formats = [
|
||||||
|
"application/vnd.mx-excel",
|
||||||
|
"application/vnd.ms-exce;.sheet.macroEnabled12",
|
||||||
|
"application/vnd.ms-excel.template.macroEnabled.12",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/vnd.oasis.opendocument.spreadsheet",
|
||||||
|
];
|
||||||
|
|
||||||
|
return formats.includes(file.mime);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isArchive = (file) => {
|
||||||
|
// matches against known archive formats
|
||||||
|
const formats = [
|
||||||
|
"application/zip",
|
||||||
|
"application/gzip",
|
||||||
|
"application/x-freearc",
|
||||||
|
"application/x-bzip",
|
||||||
|
"application/x-bzip2",
|
||||||
|
"application/vnd.rar",
|
||||||
|
"application/x-7z-compressed",
|
||||||
|
];
|
||||||
|
|
||||||
|
return formats.includes(file.mime);
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
<title inertia>{{ config('app.name') }}</title>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
|
|
||||||
7
resources/views/mail/share.blade.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Hello {{ $recipient->name }},
|
||||||
|
<br />
|
||||||
|
{{ $sender->name }} has shared the following files to you via DR⭍VE:
|
||||||
|
|
||||||
|
@foreach($files as $file)
|
||||||
|
<h3>{{$file->name}}{{ $file->is_folder ? ' - folder' : '' }}</h3>
|
||||||
|
@endforeach
|
||||||
|
|
@ -16,21 +16,30 @@ use Inertia\Inertia;
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::redirect('/', '/files');
|
||||||
return Inertia::render('Welcome', [
|
|
||||||
'canLogin' => Route::has('login'),
|
|
||||||
'canRegister' => Route::has('register'),
|
|
||||||
'laravelVersion' => Application::VERSION,
|
|
||||||
'phpVersion' => PHP_VERSION,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::controller(\App\Http\Controllers\FileController::class)->middleware(['auth', 'verified'])->group(function () {
|
Route::controller(\App\Http\Controllers\FileController::class)->middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::get('/files/{folder?}', 'userFiles')
|
Route::get('/files/{folder?}', 'userFiles')
|
||||||
->where('folder', '(.*)')
|
->where('folder', '(.*)')
|
||||||
->name('userFiles');
|
->name('userFiles');
|
||||||
|
Route::get('/shared-with-me/{folder?}', 'sharedWithMe')
|
||||||
|
->where('folder', '(.*)')
|
||||||
|
->name('sharedWith');
|
||||||
|
Route::get('/shared-by-me/{folder?}', 'sharedByMe')
|
||||||
|
->where('folder', '(.*)')
|
||||||
|
->name('sharedBy');
|
||||||
|
Route::get('/recycle-bin/{folder?}', 'recycleBin')
|
||||||
|
->where('folder', '(.*)')
|
||||||
|
->name('recycleBin');
|
||||||
Route::post('/folder/new', 'newFolder')->name('folder.new');
|
Route::post('/folder/new', 'newFolder')->name('folder.new');
|
||||||
Route::post('/file/upload', 'upload')->name('file.upload');
|
Route::post('/file/upload', 'upload')->name('file.upload');
|
||||||
|
Route::get('/file/download', 'download')->name('file.download');
|
||||||
|
Route::get('/file/download-shared', 'downloadShared')->name('file.downloadShared');
|
||||||
|
Route::post('/file/recycle', 'recycle')->name('file.recycle');
|
||||||
|
Route::post('/file/restore', 'restore')->name('file.restore');
|
||||||
|
Route::delete('/file/delete', 'delete')->name('file.delete');
|
||||||
|
Route::post('/file/share', 'share')->name('file.share');
|
||||||
|
Route::post('/file/unshare', 'unshare')->name('file.unshare');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
|
|
|
||||||