functional version

This commit is contained in:
ak 2023-10-17 22:45:04 -07:00
parent 96a62ff4e1
commit d40584de14
67 changed files with 3414 additions and 273 deletions

View file

@ -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));
}
}

View file

@ -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.

View file

@ -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;

View 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
];
}
}

View 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
]);
}
}

View 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
]);
}
}

View 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
];
}
}
}

View file

@ -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.');
}
}
],
]);
}
}

View 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;
}
}

View file

@ -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,
];
}
}

View 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 [];
}
}

View file

@ -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);
}
}
}

View file

@ -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'
];
}

View file

@ -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.

View file

@ -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();
});
}

View file

@ -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'));
});
}
};

View file

@ -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
View 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
View 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

View 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

View 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

View 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

View 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
View file

@ -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",

View file

@ -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"
}
}

View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -45,3 +45,7 @@
opacity: 0;
}
}
.errormsg {
white-space: pre-line;
}

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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&nbsp;
<span :class="plusClass">&#xFF0B;</span>&nbsp;New&nbsp;
</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
>

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -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

View file

@ -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>

View file

@ -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',
]"

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View 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">&#x27A4;</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>

View 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">&#x27A4;</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>

View 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">&#x27A4;</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>

View file

@ -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">&#x1F844;</span>&nbsp;&nbsp;&nbsp;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>

View file

@ -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>

View file

@ -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",
},
});

View 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);
};

View file

@ -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">

View 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

View file

@ -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 () {