Add modal to show paste info (web)

Done paste upload logic (web)

Add json response (paste)

Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
Joe Ma 2023-03-26 18:54:35 +08:00
parent aa154b213e
commit 576a543703
No known key found for this signature in database
GPG key ID: 7A0ECF5F5EDC587F
6 changed files with 289 additions and 51 deletions

View file

@ -4,10 +4,12 @@
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ES6MissingAwait" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="ES6MissingAwait" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlFormInputWithoutLabel" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="HtmlFormInputWithoutLabel" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JSUnresolvedVariable" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="true" level="TYPO" enabled_by_default="true"> <inspection_tool class="SpellCheckingInspection" enabled="true" level="TYPO" enabled_by_default="true">
<option name="processCode" value="false" /> <option name="processCode" value="false" />
<option name="processLiterals" value="false" /> <option name="processLiterals" value="false" />
<option name="processComments" value="true" /> <option name="processComments" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile> </profile>
</component> </component>

View file

@ -23,7 +23,8 @@ import dedent from 'dedent-js';
// Constants // Constants
const SERVICE_URL = 'pb.nekoul.com'; const SERVICE_URL = 'pb.nekoul.com';
const PASTE_INDEX_HTML_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/paste.html'; const PASTE_INDEX_HTML_URL_v1 = 'https://raw.githubusercontent.com/rikkaneko/paste/main/web/v1/paste.html';
const PASTE_INDEX_HTML_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/web/v2/paste.html';
const UUID_LENGTH = 4; const UUID_LENGTH = 4;
export interface Env { export interface Env {
@ -67,6 +68,22 @@ export default {
}); });
} }
if (path === '/v1' && method == 'GET') {
return await fetch(PASTE_INDEX_HTML_URL_v1, {
cf: {
cacheEverything: true,
},
}).then(value => {
return new Response(value.body, {
// Add the correct content-type to response header
headers: {
'content-type': 'text/html; charset=UTF-8;',
'cache-control': 'public, max-age=172800',
},
});
});
}
if (path === '/') { if (path === '/') {
switch (method) { switch (method) {
// Fetch the HTML for uploading text/file // Fetch the HTML for uploading text/file
@ -98,6 +115,7 @@ export default {
let read_limit: number | undefined; let read_limit: number | undefined;
let need_qrcode: boolean = false; let need_qrcode: boolean = false;
let paste_type: string | undefined; let paste_type: string | undefined;
let reply_json: boolean = false;
// Content-Type: multipart/form-data (deprecated) // Content-Type: multipart/form-data (deprecated)
if (content_type.includes('multipart/form-data')) { if (content_type.includes('multipart/form-data')) {
const formdata = await request.formData(); const formdata = await request.formData();
@ -142,10 +160,16 @@ export default {
// Check if qrcode generation needed // Check if qrcode generation needed
const qr = formdata.get('qrcode'); const qr = formdata.get('qrcode');
if (typeof qr === 'string' && qr.toLowerCase() === 'true' || qr === 'on') { if (typeof qr === 'string' && qr === '1') {
need_qrcode = true; need_qrcode = true;
} }
// Check reply format
const json = formdata.get('json');
if (typeof json === 'string' && json === '1') {
reply_json = true;
}
// Paste API v2 // Paste API v2
} else { } else {
title = headers.get('x-paste-title') || undefined; title = headers.get('x-paste-title') || undefined;
@ -153,6 +177,7 @@ export default {
password = headers.get('x-paste-pass') || undefined; password = headers.get('x-paste-pass') || undefined;
paste_type = headers.get('x-paste-type') || undefined; paste_type = headers.get('x-paste-type') || undefined;
need_qrcode = headers.get('x-paste-qr') === '1'; need_qrcode = headers.get('x-paste-qr') === '1';
reply_json = headers.get('x-json') === '1';
const count = headers.get('x-paste-read-limit') || ''; const count = headers.get('x-paste-read-limit') || '';
const n = parseInt(count); const n = parseInt(count);
if (isNaN(n) || n <= 0) { if (isNaN(n) || n <= 0) {
@ -235,7 +260,7 @@ export default {
// Key will be expired after 28 day if unmodified // Key will be expired after 28 day if unmodified
ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), {expirationTtl: 2419200})); ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(descriptor), {expirationTtl: 2419200}));
return await get_paste_info(uuid, descriptor, env, is_browser, need_qrcode); return await get_paste_info(uuid, descriptor, env, is_browser, need_qrcode, reply_json);
} else { } else {
return new Response('Unable to upload the paste.\n', { return new Response('Unable to upload the paste.\n', {
status: 500, status: 500,
@ -462,22 +487,49 @@ export default {
}, },
}; };
async function get_paste_info(uuid: string, descriptor: PasteIndexEntry, env: Env, use_html: boolean = true, need_qr: boolean = false): Promise<Response> { async function get_paste_info(uuid: string, descriptor: PasteIndexEntry, env: Env,
use_html: boolean = true, need_qr: boolean = false, reply_json = false): Promise<Response> {
const created = new Date(descriptor.last_modified); const created = new Date(descriptor.last_modified);
const expired = new Date(descriptor.last_modified + 2419200000); const expired = new Date(descriptor.last_modified + 2419200000);
const link = `https://${SERVICE_URL}/${uuid}`; const link = `https://${SERVICE_URL}/${uuid}`;
const paste_info = {
uuid,
link,
link_qr: 'https://qrcode.nekoul.com/?' + new URLSearchParams({q: link, type: 'svg'}),
type: descriptor.type ?? 'paste',
title: descriptor.title?.trim(),
mime_type: descriptor.mime_type,
human_readable_size: `${to_human_readable_size(descriptor.size)}`,
size: descriptor.size,
password: !!descriptor.password,
read_count_remain: descriptor.read_count_remain,
created: created.toISOString(),
expired: expired.toISOString(),
};
// Reply with JSON
if (reply_json) {
return new Response(JSON.stringify(paste_info), {
headers: {
'content-type': 'application/json; charset=utf-8',
'cache-control': 'no-store',
},
});
}
// Plain text reply
let content = dedent` let content = dedent`
id: ${uuid} uuid: ${uuid}
link: ${link} link: ${link}
type: ${descriptor.type ?? 'paste'} type: ${paste_info.type ?? 'paste'}
title: ${descriptor.title?.trim() || '-'} title: ${paste_info.title || '-'}
mime-type: ${descriptor.mime_type ?? '-'} mime-type: ${paste_info.mime_type ?? '-'}
size: ${descriptor.size} bytes (${to_human_readable_size(descriptor.size)}) size: ${paste_info.size} bytes (${paste_info.human_readable_size})
password: ${(!!descriptor.password)} password: ${paste_info.password}
remaining read count: ${descriptor.read_count_remain !== undefined ? remaining read count: ${paste_info.read_count_remain !== undefined ?
descriptor.read_count_remain ? descriptor.read_count_remain : `0 (expired)` : '-'} paste_info.read_count_remain ? paste_info.read_count_remain : `0 (expired)` : '-'}
created at ${created.toISOString()} created at ${paste_info.created}
expired at ${expired.toISOString()} expired at ${paste_info.expired}
`; `;
// Browser response // Browser response
@ -492,7 +544,7 @@ async function get_paste_info(uuid: string, descriptor: PasteIndexEntry, env: En
<body> <body>
<pre style="word-wrap: break-word; white-space: pre-wrap; <pre style="word-wrap: break-word; white-space: pre-wrap;
font-family: 'Fira Mono', monospace; font-size: 16px;">${content}</pre> font-family: 'Fira Mono', monospace; font-size: 16px;">${content}</pre>
${(need_qr) ? `<img src="${'https://qrcode.nekoul.com/?' + new URLSearchParams({q: link, type: 'svg'})}" ${(need_qr) ? `<img src="${paste_info.link_qr}"
alt="${link}" style="max-width: 280px">` : ''} alt="${link}" style="max-width: 280px">` : ''}
</body> </body>
</html> </html>

View file

@ -42,12 +42,15 @@
] ]
/* Specify type package names to be included without being referenced in a source file. */, /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */, "resolveJsonModule": true
/* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */ // "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, "allowJs": true
"checkJs": true /* Enable error reporting in type-checked JavaScript files. */, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
"checkJs": false
/* Enable error reporting in type-checked JavaScript files. */,
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */ /* Emit */

View file

@ -83,7 +83,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<input id="show_qr_checkbox" type="checkbox" name="qrcode" checked> <input id="show_qr_checkbox" type="checkbox" name="qrcode" checked value="1">
<label for="show_qr_checkbox">Show QR code on sumbitted</label> <label for="show_qr_checkbox">Show QR code on sumbitted</label>
</div> </div>
<div> <div>
@ -169,7 +169,9 @@
document.getElementById('upload_file_form').addEventListener('submit', handle_submit_form, false); document.getElementById('upload_file_form').addEventListener('submit', handle_submit_form, false);
</script> </script>
<br> <br>
<a href="https://nekoul.com">[Homepage]</a><a href="https://github.com/rikkaneko/paste#api-specification">[API]</a> <a href="https://nekoul.com">[Homepage]</a>
<a href="https://github.com/rikkaneko/paste#api-specification">[API]</a>
<a href="https://pb.nekou.com">[Switch to new version]</a>
<p>&copy; 2022 rikkaneko</p> <p>&copy; 2022 rikkaneko</p>
</body> </body>
</html> </html>

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
const endpoint = 'https://pb.nekoul.com';
let input_div = { let input_div = {
file: null, file: null,
text: null, text: null,
@ -28,7 +30,15 @@ let inputs = {
url: null, url: null,
}; };
let selected_type = 'file'; let paste_modal = {
modal: null,
uuid: null,
qrcode: null,
title: null,
expired: null,
};
let saved_modal = null;
function validate_url(path) { function validate_url(path) {
let url; let url;
@ -43,12 +53,13 @@ function validate_url(path) {
function show_pop_alert(message, alert_type = 'alert-primary') { function show_pop_alert(message, alert_type = 'alert-primary') {
remove_pop_alert(); remove_pop_alert();
$('body').prepend(jQuery.parseHTML( $('body').prepend(jQuery.parseHTML(
`<div class="alert ${alert_type} alert-dismissible position-absolute top-0 start-50 translate-middle-x outer" `<div class="alert ${alert_type} alert-dismissible fade show position-absolute top-0 start-50 translate-middle-x"
style="margin-top: 80px; max-width: 500px; width: 80%" id="pop_alert" role="alert"> \ style="margin-top: 80px; max-width: 500px; width: 80%" id="pop_alert" role="alert"> \
<div>${message}</div> \ <div>${message}</div> \
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> \ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> \
</div>`, </div>`,
)); ));
window.scrollTo(0, 0);
} }
function remove_pop_alert() { function remove_pop_alert() {
@ -57,6 +68,37 @@ function remove_pop_alert() {
alert.remove(); alert.remove();
} }
function build_paste_modal(paste_info, show_qrcode = true) {
// Show saved modal
if (!!!paste_info && !!!saved_modal) {
console.err('Invalid call to build_paste_modal().');
return;
}
if (!!!paste_info) {
saved_modal.show();
return;
}
saved_modal = null;
paste_modal.uuid.text(paste_info.link);
paste_modal.uuid.prop('href', paste_info.link);
paste_modal.qrcode.prop('src', paste_info.link_qr);
paste_modal.qrcode.prop('alt', paste_info.link);
// Hide/Show QRCode
if (!show_qrcode) paste_modal.qrcode.addClass('d-none');
else paste_modal.qrcode.removeClass('d-none');
Object.entries(paste_info).forEach(([key, val]) => {
if (key.includes('link')) return;
$(`#paste_info_${key}`).text(val ?? '-');
});
let modal = new bootstrap.Modal(paste_modal.modal);
saved_modal = modal;
modal.show();
}
$(function () { $(function () {
input_div.file = $('#file_upload_layout'); input_div.file = $('#file_upload_layout');
input_div.text = $('#text_input_layout'); input_div.text = $('#text_input_layout');
@ -64,6 +106,9 @@ $(function () {
inputs.file = $('#file_upload'); inputs.file = $('#file_upload');
inputs.text = $('#text_input'); inputs.text = $('#text_input');
inputs.url = $('#url_input'); inputs.url = $('#url_input');
paste_modal.modal = $('#paste_modal');
paste_modal.uuid = $('#paste_uuid');
paste_modal.qrcode = $('#paste_qrcode');
let file_stat = $('#file_stats'); let file_stat = $('#file_stats');
let title = $('#paste_title'); let title = $('#paste_title');
@ -73,6 +118,15 @@ $(function () {
let upload_button = $('#upload_button'); let upload_button = $('#upload_button');
let url_validate_result = $('#url_validate_result'); let url_validate_result = $('#url_validate_result');
let tos_btn = $('#tos_btn'); let tos_btn = $('#tos_btn');
let show_saved_btn = $('#show_saved_button');
let go_btn = $('#go_button');
let go_id = $('#go_paste_id');
// Enable bootstrap tooltips
const tooltip_trigger_list = [].slice.call($('[data-bs-toggle="tooltip"]'));
const tooltip_list = tooltip_trigger_list.map(function (e) {
return new bootstrap.Tooltip(e);
});
inputs.file.on('change', function () { inputs.file.on('change', function () {
inputs.file.removeClass('is-invalid'); inputs.file.removeClass('is-invalid');
@ -124,6 +178,7 @@ $(function () {
inputs.url.on('input', function () { inputs.url.on('input', function () {
inputs.url.removeClass('is-invalid'); inputs.url.removeClass('is-invalid');
url_validate_result.removeClass('text-danger'); url_validate_result.removeClass('text-danger');
url_validate_result.text('');
if (!validate_url(this.value)) { if (!validate_url(this.value)) {
inputs.url.addClass('is-invalid'); inputs.url.addClass('is-invalid');
url_validate_result.addClass('text-danger'); url_validate_result.addClass('text-danger');
@ -131,16 +186,15 @@ $(function () {
} }
}); });
upload_button.on('click', function () { upload_button.on('click', async function () {
inputs[selected_type].trigger('input');
const form = $('#upload_form')[0]; const form = $('#upload_form')[0];
const formdata = new FormData(form); let formdata = new FormData(form);
let content = {}; const type = formdata.get('paste-type');
formdata.forEach((val, key) => { const content = formdata.get('u');
content[key] = val; const show_qrcode = formdata.get('qrcode') === '1';
});
if (inputs[selected_type].hasClass('is-invalid') || !(!!content.u?.size || !!content.u?.length)) { inputs[type].trigger('input');
if (inputs[type].hasClass('is-invalid') || !(!!content?.size || !!content?.length)) {
show_pop_alert('Please check your upload file or content', 'alert-danger'); show_pop_alert('Please check your upload file or content', 'alert-danger');
return false; return false;
} }
@ -150,13 +204,71 @@ $(function () {
return false; return false;
} }
// TODO Upload to pb service switch (type) {
case 'file':
formdata.set('paste-type', 'paste');
break;
case 'text':
formdata.set('paste-type', 'paste');
break;
case 'url':
formdata.set('paste-type', 'link');
}
// Remove empty entries
let filtered = new FormData();
formdata.forEach((val, key) => {
if (!!val) filtered.set(key, val);
});
// Request JSON response
filtered.set('json', '1');
upload_button.prop('disabled', true);
upload_button.text('Uploading...');
try {
const res = await fetch(endpoint, {
method: 'POST',
body: filtered,
});
const paste_info = await res.json();
if (res.ok) {
show_pop_alert('Paste created!', 'alert-success'); show_pop_alert('Paste created!', 'alert-success');
pass_input.val('');
build_paste_modal(paste_info, show_qrcode);
show_saved_btn.prop('disabled', false);
} else {
show_pop_alert('Unable to create paste', 'alert-warning');
}
} catch (err) {
console.log('error', err);
show_pop_alert(err.message, 'alert-danger');
}
upload_button.prop('disabled', false);
upload_button.text('Upload');
});
show_saved_btn.on('click', function () {
if (!!!saved_modal) {
show_pop_alert('No saved paste found.', 'alert-warning');
return;
}
saved_modal.show();
});
go_btn.on('click', function () {
const uuid = go_id.val();
if (uuid.length !== 4) {
show_pop_alert('Invalid Paste ID.', 'alert-warning');
return;
}
window.open(`https://pb.nekoul.com/${uuid}`);
}); });
}); });
function select_input_type(name) { function select_input_type(name) {
selected_type = name;
Object.keys(input_div).forEach(key => { Object.keys(input_div).forEach(key => {
input_div[key].collapse('hide'); input_div[key].collapse('hide');
inputs[key].prop('disabled', true); inputs[key].prop('disabled', true);

View file

@ -39,15 +39,19 @@
<div class="collapse navbar-collapse" id="navbar_supported_content"> <div class="collapse navbar-collapse" id="navbar_supported_content">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" aria-current="page" href="https://nekoul.com">Home</a> <a class="nav-link active" aria-current="page" href="https://nekoul.com">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="https://github.com/rikkaneko/paste#api-specification">API</a> <a class="nav-link active" href="https://github.com/rikkaneko/paste#api-specification">API</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://pb.nekoul.com/v1">Switch to old version</a>
</li> </li>
</ul> </ul>
<form class="d-flex"> <form class="d-flex">
<input class="form-control me-2" type="search" placeholder="" aria-label="go"> <input class="form-control me-2" type="search" placeholder="Paste ID" aria-label="go" id="go_paste_id"
<button class="btn btn-outline-success" type="button">Go</button> data-bs-toggle="tooltip" data-bs-placement="bottom" title="Paste ID should be in 4 characters.">
<button class="btn btn-outline-success" type="button" id="go_button">Go</button>
</form> </form>
</div> </div>
</div> </div>
@ -58,13 +62,13 @@
<div class="mb-3"> <div class="mb-3">
<div><label class="form-label">Paste Type</label></div> <div><label class="form-label">Paste Type</label></div>
<div class="btn-group w-100" role="group" aria-label="Paste type group"> <div class="btn-group w-100" role="group" aria-label="Paste type group">
<input type="radio" class="btn-check" name="paste_type" id="paste_type_file" autocomplete="off" checked <input type="radio" class="btn-check" name="paste-type" id="paste_type_file" autocomplete="off" checked
onclick="select_input_type('file')" value="file"> onclick="select_input_type('file')" value="file">
<label class="btn btn-outline-primary" for="paste_type_file">File</label> <label class="btn btn-outline-primary" for="paste_type_file">File</label>
<input type="radio" class="btn-check" name="paste_type" id="paste_type_text" autocomplete="off" <input type="radio" class="btn-check" name="paste-type" id="paste_type_text" autocomplete="off"
onclick="select_input_type('text')" value="text"> onclick="select_input_type('text')" value="text">
<label class="btn btn-outline-primary" for="paste_type_text">Text</label> <label class="btn btn-outline-primary" for="paste_type_text">Text</label>
<input type="radio" class="btn-check" name="paste_type" id="paste_type_url" autocomplete="off" <input type="radio" class="btn-check" name="paste-type" id="paste_type_url" autocomplete="off"
onclick="select_input_type('url')" value="url"> onclick="select_input_type('url')" value="url">
<label class="btn btn-outline-primary" for="paste_type_url">URL</label> <label class="btn btn-outline-primary" for="paste_type_url">URL</label>
</div> </div>
@ -131,7 +135,7 @@
</div> </div>
<div class="form-check mb-3"> <div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="show_qrcode_checkbox" name="qrcode" checked> <input class="form-check-input" type="checkbox" id="show_qrcode_checkbox" name="qrcode" checked value="1">
<label class="form-check-label" for="show_qrcode_checkbox"> <label class="form-check-label" for="show_qrcode_checkbox">
Show QR code after upload Show QR code after upload
</label> </label>
@ -141,33 +145,37 @@
<div class="form-check mb-3"> <div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="" id="tos_btn" required> <input class="form-check-input" type="checkbox" value="" id="tos_btn" required>
<label class="form-check-label" for="tos_btn"> <label class="form-check-label" for="tos_btn">
I understand <a class="link-primary" data-bs-toggle="modal" data-bs-target="#tos_model" role="button">the terms and conditions</a> I understand <a class="link-primary" data-bs-toggle="modal" data-bs-target="#tos_modal" role="button">the terms
and conditions</a>
</label> </label>
</div> </div>
<div class="mb-3 text-end"> <div class="mb-3 text-end">
<button type="button" class="btn btn-secondary me-2" id="show_saved_button" disabled>Show saved</button>
<button type="button" class="btn btn-primary" id="upload_button">Upload</button> <button type="button" class="btn btn-primary" id="upload_button">Upload</button>
</div> </div>
</form> </form>
</div> </div>
<div class="modal fade" id="tos_model" tabindex="-1" aria-labelledby="tos_model_label" aria-hidden="true"> <div class="modal fade" id="tos_modal" tabindex="-1" aria-labelledby="tos_modal_label" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable"> <div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="tos_model_label">Terms and conditions</h5> <h5 class="modal-title" id="tos_modal_label">Terms and conditions</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<h3>Paste Service</h3> <h3>Paste Service</h3>
<p> <p>
<a href="https://pb.nekoul.com">pb.nekoul.com</a> is a pastebin-like service hosted on Cloudflare Worker.<br> <a href="https://pb.nekoul.com">pb.nekoul.com</a> is a pastebin-like service hosted on Cloudflare Worker.
This service is primarily designed for own usage and interest only.<br> This service is primarily designed for own usage and interest only.<br>
All data may be deleted or expired without any notification and guarantee. Please <b>DO NOT</b> abuse this All data may be deleted or expired without any notification and guarantee. Please <b>DO NOT</b> abuse this
service.<br> service.
The limit for file upload is <b>10 MB</b> and the paste will be kept for <b>28 days</b> only by default.<br> The limit for file upload is <b>10 MB</b> and the paste will be kept for <b>28 days</b> only by default.<br>
The source code is available in my GitHub repository <a href="https://github.com/rikkaneko/paste">[here]</a>.<br> The source code is available in my GitHub repository <a
href="https://github.com/rikkaneko/paste">[here]</a>.<br>
This webpage is designed for upload files only. This webpage is designed for upload files only.
For other operations like changing paste settings and deleting paste, please make use of the For other operations like changing paste settings and deleting paste, please make use of the
<a href="https://github.com/rikkaneko/paste#api-specification">API call</a> with <a <a href="https://github.com/rikkaneko/paste#api-specification">API call</a> with <a
@ -175,7 +183,66 @@
</p> </p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="paste_modal" tabindex="-1" aria-labelledby="paste_modal_label" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="paste_modal_label">Paste</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2 text-center fs-4">
<a href="" class="link-primary" id="paste_uuid"></a>
</div>
<img src="" class="mb-3 rounded mx-auto d-block w-75" alt="" id="paste_qrcode" style="max-width: 280px">
<div class="mb-3 w-75 mx-auto">
<table class="table table-striped table-bordered align-middle caption-top">
<caption>Paste information</caption>
<tbody>
<tr>
<td class="text-center col-3">Paste ID</td>
<td class="text-center col-6" id="paste_info_uuid">-</td>
</tr>
<tr>
<td class="text-center col-3">Title</td>
<td class="text-center col-6" id="paste_info_title">-</td>
</tr>
<tr>
<td class="text-center col-3">Type</td>
<td class="text-center col-6" id="paste_info_type">-</td>
</tr>
<tr>
<td class="text-center col-3">Size</td>
<td class="text-center col-6" id="paste_info_human_readable_size">-</td>
</tr>
<tr>
<td class="text-center col-3">Password</td>
<td class="text-center col-6" id="paste_info_password">-</td>
</tr>
<tr>
<td class="text-center col-3">Read limit</td>
<td class="text-center col-6" id="paste_info_read_count_remain">-</td>
</tr>
<tr>
<td class="text-center col-3">Created</td>
<td class="text-center col-6" id="paste_info_created">-</td>
</tr>
<tr>
<td class="text-center col-3">Expired</td>
<td class="text-center col-6" id="paste_info_expired">-</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>