functional version
|
|
@ -2,17 +2,41 @@
|
|||
|
||||
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\Models\File;
|
||||
use Inertia\Inertia;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
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
|
||||
{
|
||||
// render user's files
|
||||
public function userFiles(string $folder = null) {
|
||||
public function userFiles(Request $request, string $folder = null) {
|
||||
// validate passed in $folder param
|
||||
// if passed in,
|
||||
if ($folder) {
|
||||
|
|
@ -25,18 +49,25 @@ class FileController extends Controller
|
|||
// use root folder if $folder not passed in
|
||||
$folder = $this->getRoot();
|
||||
}
|
||||
// get all files and folders where parent_id = root
|
||||
$files = File::query()->where('parent_id', $folder->id)
|
||||
// that are created by the current user
|
||||
->where('created_by', Auth::id())
|
||||
|
||||
// get all files and folders created by current user
|
||||
$files = File::query()->where('created_by', Auth::id())
|
||||
// that have not been deleted
|
||||
->whereNull('deleted_at')
|
||||
// order by folders first
|
||||
->orderByDesc('is_folder')
|
||||
// then order by creation date
|
||||
->orderByDesc('created_at')
|
||||
// return x amount (default 8)
|
||||
->paginate(8);
|
||||
->orderByDesc('created_at');
|
||||
|
||||
$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);
|
||||
|
||||
|
|
@ -47,30 +78,838 @@ class FileController extends Controller
|
|||
return Inertia::render('UserFiles', compact('files', 'folder', 'ancestors'));
|
||||
}
|
||||
|
||||
// create new folder based on passed in StoreFolderRequest
|
||||
public function newFolder(StoreFolderRequest $request) {
|
||||
// render files shared with user
|
||||
public function sharedWithMe(Request $request, $folder = null) {
|
||||
// validate passed in $folder param
|
||||
if ($folder) {
|
||||
// get ancestors
|
||||
$firstQuery = File::query()->join('file_shares', 'file_shares.file_id', 'files.id')
|
||||
->where('file_shares.user_id', 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('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'));
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
$data = $request->validated();
|
||||
// get parent folder from StoreFolderRequest
|
||||
// get parent folder from NewFolder request
|
||||
$parent = $request->parent;
|
||||
// check for parent
|
||||
if (!$parent) {
|
||||
$parent = $this->getRoot();
|
||||
}
|
||||
$file = new File();
|
||||
$file->is_folder = 1;
|
||||
$file->name = $data['name'];
|
||||
$parent->appendNode($file);
|
||||
// make new folder object based on File() object
|
||||
$folder = new File();
|
||||
$folder->is_folder = 1;
|
||||
$folder->name = $data['name'];
|
||||
|
||||
$parent->appendNode($folder);
|
||||
}
|
||||
|
||||
public function upload(StoreFileRequest $request) {
|
||||
// upload files
|
||||
public function upload(UploadFiles $request) {
|
||||
// get validated data
|
||||
$data = $request->validated();
|
||||
// get parent folder from StoreFolderRequest
|
||||
dd ($data);
|
||||
// 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() {
|
||||
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;
|
||||
|
||||
use App\Http\Requests\ParentIDBaseRequest;
|
||||
use App\Http\Requests\ParentId;
|
||||
use App\Models\File;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
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.
|
||||
|
|
@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Auth;
|
|||
use App\Models\File;
|
||||
|
||||
|
||||
class ParentIDBaseRequest extends FormRequest
|
||||
class ParentId extends FormRequest
|
||||
{
|
||||
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
|
||||
{
|
||||
// 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 [
|
||||
"mime" => $this->mime,
|
||||
"mime" => $this->mimetype,
|
||||
"path" => $this->path,
|
||||
"id" => $this->id,
|
||||
"parent_id" => $this->parent_id,
|
||||
"name" => $this->name,
|
||||
"size" => $this->size,
|
||||
"size" => $this->formatSize($size),
|
||||
"owner" => $this->user->name,
|
||||
"is_folder" => $this->is_folder,
|
||||
"created_at" => $this->created_at->diffForHumans(),
|
||||
"updated_at" => $this->updated_at->diffForHumans(),
|
||||
"created_by" => $this->created_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\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Kalnoy\Nestedset\NodeTrait;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Kalnoy\NestedSet\DescendantsRelation;
|
||||
|
||||
class File extends Model
|
||||
{
|
||||
|
|
@ -36,6 +39,35 @@ class File extends Model
|
|||
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
|
||||
protected static function boot()
|
||||
{
|
||||
|
|
@ -58,5 +90,52 @@ class File extends Model
|
|||
// append current file or folder name to path 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
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'file_id',
|
||||
'user_id',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
public const HOME = '/my-files';
|
||||
public const HOME = '/files';
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$table->id();
|
||||
$table->foreignId('fileID')->constrained('files');
|
||||
$table->foreignId('userID')->constrained('users');
|
||||
$table->foreignId('file_id')->constrained('files');
|
||||
$table->foreignId('user_id')->constrained('users');
|
||||
$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:
|
||||
laravel.test:
|
||||
build:
|
||||
context: ./vendor/laravel/sail/runtimes/8.2
|
||||
context: ./docker/8.2
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP}'
|
||||
|
|
@ -40,7 +40,7 @@ services:
|
|||
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||
volumes:
|
||||
- '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:
|
||||
- sail
|
||||
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": {
|
||||
"": {
|
||||
"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",
|
||||
"@heroicons/vue": "^2.0.18",
|
||||
"fontawesome-free": "^1.0.4",
|
||||
"mitt": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -397,6 +402,60 @@
|
|||
"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": {
|
||||
"version": "1.7.16",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -18,8 +18,13 @@
|
|||
"vue": "^3.2.41"
|
||||
},
|
||||
"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",
|
||||
"@heroicons/vue": "^2.0.18",
|
||||
"fontawesome-free": "^1.0.4",
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
||||
.errormsg {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ const props = defineProps({
|
|||
|
||||
const classes = computed(() =>
|
||||
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 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 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 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>
|
||||
|
||||
|
|
|
|||
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">
|
||||
<NavLink
|
||||
:href="route('userFiles')"
|
||||
:active="$page.url === '/my-files'"
|
||||
:active="new RegExp('files.*', 'gi').test($page.url)"
|
||||
>My Files</NavLink
|
||||
>
|
||||
<NavLink href="/">Shared with Me</NavLink>
|
||||
<NavLink href="/">Shared by Me</NavLink>
|
||||
<NavLink href="/">Recycle Bin</NavLink>
|
||||
<NavLink
|
||||
:href="route('sharedWith')"
|
||||
: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>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,36 @@
|
|||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/vue";
|
||||
import NewFolderModal from "./NewFolderModal.vue";
|
||||
import UploadFiles from "./UploadFiles.vue";
|
||||
import UploadFolder from "./UploadFolder.vue";
|
||||
|
||||
const newFolderModalActive = ref(false);
|
||||
const rotate = ref(false);
|
||||
|
||||
const toggleNewFolderModal = (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>
|
||||
|
||||
<template>
|
||||
<Menu as="div" class="relative inline-block text-left">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -31,14 +43,14 @@ const toggleNewFolderModal = (bool) => {
|
|||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<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">
|
||||
<MenuItem v-slot="{ active }"
|
||||
><a
|
||||
href="#"
|
||||
@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
|
||||
></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>
|
||||
import { useForm } from "@inertiajs/vue3";
|
||||
import { router, useForm } from "@inertiajs/vue3";
|
||||
import TextInput from "../TextInput.vue";
|
||||
import { ref } from "vue";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const form = useForm({
|
||||
search: "",
|
||||
const query = ref("");
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<form class="w-full h-[5rem] flex items-center">
|
||||
<div class="w-full h-[5rem] flex items-center">
|
||||
<TextInput
|
||||
type="text"
|
||||
class="block w-full mr-2"
|
||||
v-model="form.search"
|
||||
v-model="query"
|
||||
autocomplete
|
||||
placeholder="Search for files or folders"
|
||||
@keyup.enter.prevent="onChange()"
|
||||
></TextInput>
|
||||
</form>
|
||||
</div>
|
||||
</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"
|
||||
type="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
|
||||
/>
|
||||
<label
|
||||
|
|
|
|||
|
|
@ -1,25 +1,31 @@
|
|||
<script setup>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<MenuItem v-slot="{ active }"
|
||||
><a
|
||||
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
|
||||
<input
|
||||
@change="upload"
|
||||
type="file"
|
||||
id="file"
|
||||
class="absolute left-0 top-0 bottom-0 right-0 opacity-0"
|
||||
id="folder"
|
||||
class="absolute left-0 top-2 bottom-0 right-0 opacity-0"
|
||||
multiple
|
||||
directory
|
||||
webkitdirectory
|
||||
/>
|
||||
<label
|
||||
for="file"
|
||||
for="folder"
|
||||
class="opacity-0 absolute left-0 top-0 bottom-0 right-0 cursor-pointer"
|
||||
></label>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import ResponsiveNavLink from "../ResponsiveNavLink.vue";
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Menu as="div" class="relative inline-block text-left">
|
||||
<Menu as="div" class="relative inline-block text-left z-50">
|
||||
<div>
|
||||
<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 }}
|
||||
<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">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<ResponsiveNavLink
|
||||
:href="route('profile.edit')"
|
||||
:href="route('profile')"
|
||||
:class="[
|
||||
'group flex w-full items-center rounded-md py-2',
|
||||
]"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import Navigation from "@/Components/custom/Navigation.vue";
|
||||
import SearchForm from "@/Components/custom/SearchForm.vue";
|
||||
import UserDropdown from "@/Components/custom/UserDropdown.vue";
|
||||
import FileUploadErrorModal from "@/Components/custom/FileUploadErrorModal.vue";
|
||||
import { emitter } from "@/emitter.js";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useForm, usePage } from "@inertiajs/vue3";
|
||||
|
|
@ -10,10 +11,15 @@ import { useForm, usePage } from "@inertiajs/vue3";
|
|||
const page = usePage();
|
||||
// make refs
|
||||
const dragging = ref(false);
|
||||
const fileUploadError = ref(false);
|
||||
const draggable = ref(false);
|
||||
|
||||
// add Mitt event listener on component load
|
||||
onMounted(() => {
|
||||
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
|
||||
|
|
@ -31,6 +37,7 @@ const onDrop = (event) => {
|
|||
// create form input for file upload
|
||||
const fileUpload = useForm({
|
||||
files: [],
|
||||
paths: [],
|
||||
parent_id: null,
|
||||
});
|
||||
|
||||
|
|
@ -39,9 +46,20 @@ const uploadFiles = (files) => {
|
|||
// fill out form
|
||||
fileUpload.files = files;
|
||||
fileUpload.parent_id = page.props.folder.id;
|
||||
fileUpload.paths = Array.from(files).map((file) => file.webkitRelativePath);
|
||||
|
||||
// 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>
|
||||
|
||||
|
|
@ -49,6 +67,7 @@ const uploadFiles = (files) => {
|
|||
<div class="h-screen w-full bg-zinc-900 text-gray-100 flex gap-4">
|
||||
<Navigation />
|
||||
<main
|
||||
v-if="draggable"
|
||||
@drop.prevent="onDrop"
|
||||
@dragover.prevent="dragging = true"
|
||||
@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"
|
||||
>
|
||||
<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>
|
||||
|
|
@ -71,9 +90,32 @@ const uploadFiles = (files) => {
|
|||
<UserDropdown />
|
||||
</div>
|
||||
<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 />
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import GuestLayout from "@/Layouts/GuestLayout.vue";
|
|||
import InputError from "@/Components/InputError.vue";
|
||||
import InputLabel from "@/Components/InputLabel.vue";
|
||||
import TextInput from "@/Components/TextInput.vue";
|
||||
import { Head, Link, useForm } from "@inertiajs/vue3";
|
||||
import { Head, Link, useForm, router } from "@inertiajs/vue3";
|
||||
|
||||
defineProps({
|
||||
canResetPassword: {
|
||||
|
|
@ -21,9 +21,11 @@ const form = useForm({
|
|||
remember: false,
|
||||
});
|
||||
|
||||
// if log in is a success, reroute to user files
|
||||
const submit = () => {
|
||||
form.post(route("login"), {
|
||||
onFinish: () => form.reset("password"),
|
||||
onSuccess: () => router.visit(route("/files")),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
@ -79,11 +81,10 @@ const submit = () => {
|
|||
|
||||
<div class="flex items-center justify-end mt-4 gap-6">
|
||||
<Link
|
||||
v-if="canResetPassword"
|
||||
:href="route('password.request')"
|
||||
:href="route('register')"
|
||||
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>
|
||||
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import GuestLayout from "@/Layouts/GuestLayout.vue";
|
|||
import InputError from "@/Components/InputError.vue";
|
||||
import InputLabel from "@/Components/InputLabel.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({
|
||||
name: "",
|
||||
|
|
@ -15,6 +15,12 @@ const form = useForm({
|
|||
const submit = () => {
|
||||
form.post(route("register"), {
|
||||
onFinish: () => form.reset("password", "password_confirmation"),
|
||||
onSuccess: () => {
|
||||
alert(
|
||||
"Registration successful. Please check your e-mails for login link."
|
||||
);
|
||||
router.visit(route("/login"));
|
||||
},
|
||||
});
|
||||
};
|
||||
</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>
|
||||
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 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,
|
||||
folder: Object,
|
||||
ancestors: Array,
|
||||
});
|
||||
|
||||
const openFolder = (file) => {
|
||||
if (!file.is_folder) return;
|
||||
// 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 (!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(
|
||||
route("userFiles", {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Head title="Welcome" />
|
||||
<AuthenticatedLayout>
|
||||
<nav class="flex items-center justify-between mb-2">
|
||||
<ol class="inline-flex items-center">
|
||||
<Link
|
||||
class="border-gray-700 border px-3 py-2 rounded hover:border-sky-600"
|
||||
<MainBreadcrumbButton
|
||||
:href="route('userFiles')"
|
||||
:active="$page.url === '/files'"
|
||||
>
|
||||
<HomeIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</Link>
|
||||
</MainBreadcrumbButton>
|
||||
<li
|
||||
v-for="(ancestor, index) of ancestors.data"
|
||||
:key="ancestor.id"
|
||||
|
|
@ -57,26 +158,56 @@ const openFolder = (file) => {
|
|||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="flex gap-4">
|
||||
<ShareButton
|
||||
:shareall="allSelected"
|
||||
:selected="currentlySelected"
|
||||
/>
|
||||
<DownloadButton
|
||||
:getall="allSelected"
|
||||
:selected="currentlySelected"
|
||||
/>
|
||||
<RecycleButton
|
||||
:wipeall="allSelected"
|
||||
:selected="currentlySelected"
|
||||
/>
|
||||
</div>
|
||||
</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">
|
||||
<table class="w-full border-separate">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<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="p-3 rounded bg-zinc-600 rounded font-medium">
|
||||
<th
|
||||
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||
>
|
||||
Modified
|
||||
</th>
|
||||
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
||||
<th
|
||||
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||
>
|
||||
Owner
|
||||
</th>
|
||||
<th class="p-3 rounded bg-zinc-600 rounded font-medium">
|
||||
<th
|
||||
class="py-3 px-1 rounded bg-zinc-600 rounded font-medium"
|
||||
>
|
||||
Size
|
||||
</th>
|
||||
</tr>
|
||||
|
|
@ -85,38 +216,60 @@ const openFolder = (file) => {
|
|||
<tr
|
||||
v-for="file of files.data"
|
||||
:key="file.id"
|
||||
@click="openFolder(file)"
|
||||
@dblclick="openFile(file)"
|
||||
>
|
||||
<td
|
||||
class="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||
class="text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600 w-12"
|
||||
@click="selectFile(file)"
|
||||
>
|
||||
{{ file.name }}
|
||||
<Checkbox
|
||||
:checked="fileSelectedStatus[file.id]"
|
||||
:v-model="
|
||||
fileSelectedStatus[file.id] || allSelected
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||
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 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="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||
>
|
||||
{{ file.owner /* get name */ }}
|
||||
{{ file.owner }}
|
||||
</td>
|
||||
<td
|
||||
class="p-3 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||
class="py-3 px-1 text-center whitespace-nowrap border-gray-700 border rounded hover:border-sky-600 hover:ring-sky-600"
|
||||
>
|
||||
{{ file.size == null ? 0 : file.size }}
|
||||
{{ file.size }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="h-full" v-else>
|
||||
<div class="text-lg mt-1.5">
|
||||
<span class="arrow">🡄</span> click here
|
||||
to upload your file
|
||||
to upload your files or drag and drop below
|
||||
</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
|
||||
<div class="text-base">drop your files here</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 '../css/app.css';
|
||||
import "./bootstrap";
|
||||
import "../css/app.css";
|
||||
|
||||
import { createApp, h } from 'vue';
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
|
||||
import { createApp, h } from "vue";
|
||||
import { createInertiaApp } from "@inertiajs/vue3";
|
||||
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
|
||||
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({
|
||||
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 }) {
|
||||
return createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(ZiggyVue, Ziggy)
|
||||
.component("font-awesome-icon", FontAwesomeIcon)
|
||||
.mount(el);
|
||||
},
|
||||
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 name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||
<title inertia>{{ config('app.name') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<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 () {
|
||||
return Inertia::render('Welcome', [
|
||||
'canLogin' => Route::has('login'),
|
||||
'canRegister' => Route::has('register'),
|
||||
'laravelVersion' => Application::VERSION,
|
||||
'phpVersion' => PHP_VERSION,
|
||||
]);
|
||||
});
|
||||
Route::redirect('/', '/files');
|
||||
|
||||
Route::controller(\App\Http\Controllers\FileController::class)->middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/files/{folder?}', 'userFiles')
|
||||
->where('folder', '(.*)')
|
||||
->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('/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 () {
|
||||
|
|
|
|||