initial scrapbook implementation

This commit is contained in:
zefie
2025-07-11 14:47:48 -04:00
parent 2d77b10710
commit 1e26ffcf9d
8 changed files with 773 additions and 13 deletions

View File

@@ -2,7 +2,7 @@ var minisrv_service_file = true;
var files = session_data.pagestore.listScrapbook();
var dir = session_data.pagestore.scrapbookDir()
var start = 12;
var start = 0;
headers = `200 OK
Connection: Keep-Alive
@@ -158,14 +158,21 @@ Choose one of your saved images to view it full size.
<table cellspacing=24 cellpadding=1 width=372 background="/ROMCache/light_blue_tile.gif">
<tr>
`
for (let i = 0; i < 12; i++) {
if (files.length === 0) {
data += `<td align=center valign=middle colspan=4>
<font color=AEBFD1 size=+1><blackface> Your scrapbook is empty. </blackface></font>
</td>`;
} else {
for (let i = start; i < Math.min(files.length, start + 12); i++) {
data += `
<td align=center valign=middle>
<A href=
"${dir + files.i}" transition=light>
<img src="${dir + files.i}" width=90>
"wtv-tricks:/view-scrapbook-image?id=${files[i]}" transition=light>
<img src="wtv-tricks:/view-scrapbook-image?id=${files[i]}&width=90" width=90>
</A>
</td>`
</td>
${i % 4 === 1 ? '</tr><tr>' : ''}`
}
}
data += `
</table>

View File

@@ -0,0 +1,89 @@
var minisrv_service_file = true;
var request_is_async = true;
function handleError(reason) {
var errpage = wtvshared.doErrorPage(400, reason);
sendToClient(socket, errpage[0], errpage[1]);
}
if (!request_headers.query.url) {
handleError('No URL provided');
} else {
var url = request_headers.query.url;
function isValidImageType(contentType, url) {
// Check content-type header or file extension
if (contentType) {
return contentType === 'image/jpeg' || contentType == 'image/jpg' || contentType === 'image/gif';
}
return url.endsWith('.jpg') || url.endsWith('.jpeg') || url.endsWith('.gif');
}
try {
const parsedUrl = new URL(url);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
protocol.get(url, (res) => {
if (res.statusCode !== 200) {
handleError('URL does not exist or returned status ' + res.statusCode);
res.resume();
return;
}
const contentType = res.headers['content-type'];
const contentLength = parseInt(res.headers['content-length'], 10);
if (!isValidImageType(contentType, url)) {
handleError('URL is not a JPEG or GIF image');
res.resume();
return;
}
if (contentLength && contentLength > 1024 * 1024) {
handleError('Image is larger than 1MB');
res.resume();
return;
}
let data = [];
let totalLength = 0;
res.on('data', (chunk) => {
totalLength += chunk.length;
if (totalLength > 1024 * 1024) {
handleError('Image is larger than 1MB');
res.destroy();
return;
}
data.push(chunk);
});
res.on('end', () => {
if (totalLength > 1024 * 1024) return;
data = Buffer.concat(data);
var id = session_data.pagestore.getFreeScrapbookID();
var result = session_data.pagestore.addToScrapbook(id, contentType, data);
if (result) {
var successScrapbook = new clientShowAlert({
'image': minisrv_config.config.service_logo,
'message': "The image has been added to your scrapbook. Would you like to view your scrapbook now?",
'buttonlabel1': "No",
'buttonaction1': "client:donothing",
'buttonlabel2': "Yes",
'buttonaction2': "wtv-author:/scrapbook",
}).getURL();
sendToClient(socket, {'Status': 302, 'Location': successScrapbook, 'wtv-visit': successScrapbook}, '');
} else {
handleError('Failed to add image to scrapbook');
}
});
res.on('error', (err) => {
handleError('Error downloading image');
});
}).on('error', (err) => {
handleError('Failed to fetch URL');
});
} catch (e) {
handleError(e.message);
}
}

View File

@@ -0,0 +1,35 @@
var minisrv_service_file = true;
var request_is_async = true;
function handleError(reason) {
var errpage = wtvshared.doErrorPage(400, reason);
sendToClient(socket, errpage[0], errpage[1]);
}
async function handleImage() {
if (!request_headers.query.id) {
handleError('No image ID specified');
} else {
data = session_data.pagestore.getScrapbookImage(request_headers.query.id);
if (!data) {
handleError('Image not found');
} else {
try {
if (request_headers.query.width) {
// Scale the image to the specified width without losing aspect ratio, without using wtvshared
const width = parseInt(request_headers.query.width, 10);
data = await sharp(data).resize({ width, withoutEnlargement: true }).toBuffer();
}
headers = `200 OK
Content-Type: ${session_data.pagestore.getScrapbookImageType(request_headers.query.id)}
Content-Length: ${data.length}`
sendToClient(socket, headers, data);
} catch (error) {
handleError('Error processing image');
console.error('Image processing error:', error);
}
}
}
}
handleImage();

View File

@@ -10,6 +10,7 @@ class WTVAuthor {
wtvclient = null;
pageFileExt = ".page";
pagestore_dir = null;
scrapbook_dir = null;
pageArr = [];
blockArr = [];
header = null;
@@ -59,16 +60,26 @@ class WTVAuthor {
if (this.pagestore_dir === null) {
// set pagestore directory local var so we don't call the function every time
var userstore_dir = this.wtvclient.getUserStoreDirectory();
this.debug("pagestoreExists", "userstore_dir", userstore_dir)
// PageStore
var store_dir = "PageStore" + this.path.sep;
this.pagestore_dir = userstore_dir + store_dir;
this.pagestore_dir = userstore_dir + store_dir;
}
return this.fs.existsSync(this.pagestore_dir);
}
return true;
}
scrapbookExists() {
if (!this.isguest) {
if (this.scrapbook_dir === null) {
var userstore_dir = this.wtvclient.getUserStoreDirectory();
var store_dir = "Scrapbook" + this.path.sep;
this.scrapbook_dir = userstore_dir + store_dir;
}
}
return this.fs.existsSync(this.scrapbook_dir);
}
createPagestore() {
if (this.pagestoreExists() === false) {
@@ -79,7 +90,93 @@ class WTVAuthor {
}
return false;
}
createScrapbook() {
if (this.scrapbookExists() === false) {
try {
if (!this.fs.existsSync(this.scrapbook_dir)) this.fs.mkdirSync(this.scrapbook_dir, { recursive: true });
return true;
} catch { }
}
return false
}
scrapbookDir() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
return this.scrapbook_dir;
}
listScrapbook() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
const files = this.fs.readdirSync(this.scrapbook_dir);
const filteredFiles = files.filter(file => !file.endsWith('.meta'));
return filteredFiles;
}
getFreeScrapbookID() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var id = 1;
var files = this.fs.readdirSync(this.scrapbook_dir);
if (files.length == 0) {
return id;
}
files = files.map(file => parseInt(file.substr(0, file.indexOf('.'))));
while (files.includes(id)) {
id++;
}
return id;
}
getScrapbookImage(id) {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var file = this.scrapbook_dir + id;
if (this.fs.existsSync(file)) {
return this.fs.readFileSync(file);
}
return null;
}
getScrapbookImageType(id) {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var file = this.scrapbook_dir + id + ".meta";
if (this.fs.existsSync(file)) {
var meta = this.fs.readFileSync(file, 'utf8');
try {
var metaData = JSON.parse(meta);
return metaData.contentType;
} catch (e) {
this.debug("getScrapbookImageType", "Error parsing metadata for image ID", id, e);
}
}
return null;
}
addToScrapbook(filename, contentType, data) {
try {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var fileout = this.scrapbook_dir + filename;
var fileout_meta = this.scrapbook_dir + filename + ".meta";
this.fs.writeFileSync(fileout, data);
this.fs.writeFileSync(fileout_meta, JSON.stringify({
"contentType": contentType
}));
return true;
} catch {}
return false;
}
createPage(style) {
this.pagestoreExists()
var pagestorepath = this.pagestore_dir;

View File

@@ -703,7 +703,6 @@ class WTVShared {
return encoded.toUpperCase();
}
/**
* Decodes a urlencoded string into a binary buffer
* @param {string} encoded urlencoded string