added ios share integration and rudimentary upload

This commit is contained in:
slynn1324 2021-01-28 11:43:53 -06:00
parent acc3f0b732
commit 27e48a1822
10 changed files with 450 additions and 59 deletions

265
server.js
View file

@ -1,3 +1,5 @@
( async () => {
const yargs = require('yargs');
const express = require('express');
const bodyParser = require('body-parser');
@ -57,6 +59,9 @@ const DB_PATH = path.resolve(argv['db-path']);
const IMAGE_PATH = path.resolve(argv['image-path']);
const PORT = argv.port;
const THUMBNAIL_IMAGE_SIZE = 400;
const ADDITIONAL_IMAGE_SIZES = [800,1280,1920,2560];
console.log('tinypin starting...');
console.log('');
console.log(`version: ${VERSION}`);
@ -74,7 +79,7 @@ console.log('');
const db = betterSqlite3(DB_PATH);
initDb();
await initDb();
const COOKIE_KEY = Buffer.from(db.prepare("SELECT value FROM properties WHERE key = ?").get('cookieKey').value, 'hex');
@ -82,8 +87,10 @@ const COOKIE_KEY = Buffer.from(db.prepare("SELECT value FROM properties WHERE ke
const app = express();
app.use(express.static('public'));
app.use(bodyParser.raw({type: 'image/jpeg', limit: '25mb'}));
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json());
app.set('json spaces', 2);
app.use(cookieParser());
@ -121,9 +128,39 @@ function decryptCookie(ciphertext){
// handle auth
app.use ( async (req, res, next) => {
// disable security
// req.user = {
// id: 1,
// name: 'a'
// };
// next();
// return;
if ( req.originalUrl.startsWith("/up/") ){
console.log("got up!");
console.log("content type = " + req.headers['content-type']);
console.log(typeof(req.body));
await fs.writeFile('up.jpg', req.body);
res.send(OK);
return;
}
if ( req.originalUrl.startsWith("/images/") ){
req.user = {
id: 1,
name: "a"
};
next();
return;
}
// skip auth for pub resources
// handle login and register paths
if ( req.originalUrl.startsWith("/pub/")){
if ( req.originalUrl.startsWith("/pub/") ){
next();
return;
} if ( req.method == "GET" && req.originalUrl == "/login" ){
@ -224,11 +261,21 @@ app.use( (req, res, next) => {
res.setHeader('Cache-control', `private, max-age=2592000000`); // 30 days
res.sendFile(filepath);
} else if ( req.method == "GET" && req.originalUrl.startsWith("/dl/") ){
let path = req.originalUrl.replace("/dl/", "/images/");
let filepath = IMAGE_PATH + "/" + req.user.id + "/" + path;
res.setHeader("Content-Disposition", 'attachment; filename="image.jpg');
res.sendFile(filepath);
} else {
next();
}
});
app.use(express.static('static'));
//emulate slow down
@ -332,8 +379,15 @@ app.delete("/api/boards/:boardId", async (req, res) => {
let pins = db.prepare("SELECT id FROM pins WHERE userId = @userId and boardId = @boardId").all({userId:req.user.id, boardId:req.params.boardId});
for ( let i = 0; i < pins.length; ++i ){
await fs.unlink(getThumbnailImagePath(req.user.id, pins[i].id).file);
await fs.unlink(getOriginalImagePath(req.user.id, pins[i].id).file);
await fs.unlink(getImagePath(req.user.id, pins[i].id, 'o').file);
await fs.unlink(getImagePath(req.user.id, pins[i].id, THUMBNAIL_IMAGE_SIZE).file);
for ( let s = 0; s < ADDITIONAL_IMAGE_SIZES.length; ++s ){
await fs.unlink(getImagePath(req.user.id, pins[i].id, ADDITIONAL_IMAGE_SIZES[s]).file);
}
}
let result = db.prepare("DELETE FROM pins WHERE userId = @userId and boardId = @boardId").run({userId:req.user.id, boardId:req.params.boardId});
@ -369,6 +423,7 @@ app.get("/api/pins/:pinId", (req, res) => {
app.post("/api/pins", async (req, res) => {
try {
// download the image first to make sure we can get it
let image = await downloadImage(req.body.imageUrl);
let result = db.prepare(`INSERT INTO PINS (
@ -411,15 +466,7 @@ app.post("/api/pins", async (req, res) => {
let id = result.lastInsertRowid;
// write the images to disk
let originalImagePath = getOriginalImagePath(req.user.id, id);
let thumbnailImagePath = getThumbnailImagePath(req.user.id, id);
await fs.mkdir(originalImagePath.dir, {recursive: true});
await fs.mkdir(thumbnailImagePath.dir, {recursive: true});
await fs.writeFile(originalImagePath.file, image.original.buffer);
console.log(`Saved original to: ${originalImagePath.file}`);
await fs.writeFile(thumbnailImagePath.file, image.thumbnail.buffer);
console.log(`Saved thumbnail to: ${thumbnailImagePath.file}`);
await saveImage(req.user.id, id, image);
// return the newly created row
let pin = db.prepare("SELECT * FROM pins WHERE userId = @userId and id = @pinId").get({userId: req.user.id, pinId: id});
@ -468,8 +515,12 @@ app.delete("/api/pins/:pinId", async (req, res) => {
let result = db.prepare('DELETE FROM pins WHERE userId = @userId and id = @pinId').run({userId: req.user.id, pinId:req.params.pinId});
if ( result.changes == 1 ){
await fs.unlink(getThumbnailImagePath(req.user.id, req.params.pinId).file);
await fs.unlink(getOriginalImagePath(req.user.id, req.params.pinId).file);
await fs.unlink(getImagePath(req.user.id, req.params.pinId, 'o').file);
await fs.unlink(getImagePath(req.user.id, req.params.pinId, THUMBNAIL_IMAGE_SIZE).file);
for ( let s = 0; s < ADDITIONAL_IMAGE_SIZES.length; ++s ){
await fs.unlink(getImagePath(req.user.id, req.params.pinId, ADDITIONAL_IMAGE_SIZES[s]).file);
}
console.log(`deleted pin#${req.params.pinId}`);
res.send(OK);
@ -482,6 +533,77 @@ app.delete("/api/pins/:pinId", async (req, res) => {
}
});
app.post("/up/", async (req, res) => {
console.log("got up!");
console.log("content type = " + req.headers['content-type']);
let boardName = req.headers['board-name'].trim();
console.log("board name = " + req.headers['board-name']);
console.log(typeof(req.body));
let result = db.prepare("SELECT id FROM boards WHERE name = @name and userId = @userId").get({name: boardName, userId: req.user.id});
let boardId = null;
if ( result ){
boardId = result.id;
} else {
result = db.prepare("INSERT INTO boards (name, userId, hidden, createDate) VALUES (@name, @userId, @hidden, @createDate)").run({name: boardName, userId: req.user.id, hidden: null, createDate: new Date().toISOString()});
boardId = result.lastInsertRowid;
}
console.log("boardId=" + boardId);
let image = await processImage(req.body);
result = db.prepare(`INSERT INTO PINS (
boardId,
imageUrl,
siteUrl,
description,
sortOrder,
originalHeight,
originalWidth,
thumbnailHeight,
thumbnailWidth,
userId,
createDate
) VALUES (
@boardId,
@imageUrl,
@siteUrl,
@description,
@sortOrder,
@originalHeight,
@originalWidth,
@thumbnailHeight,
@thumbnailWidth,
@userId,
@createDate)
`).run({
boardId: boardId,
imageUrl: null,
siteUrl: null,
description: null,
sortOrder: null,
originalHeight: image.original.height,
originalWidth: image.original.width,
thumbnailHeight: image.thumbnail.height,
thumbnailWidth: image.thumbnail.width,
userId: req.user.id,
createDate: new Date().toISOString()
});
let id = result.lastInsertRowid;
await saveImage(req.user.id, id, image);
// return the newly created row
let pin = db.prepare("SELECT * FROM pins WHERE userId = @userId and id = @pinId").get({userId: req.user.id, pinId: id});
res.send(pin);
return;
});
// start listening
@ -490,7 +612,7 @@ app.listen(PORT, () => {
console.log('');
});
function initDb(){
async function initDb(){
console.log("initializing database...");
@ -600,10 +722,67 @@ function initDb(){
})();
}
if ( schemaVersion < 3 ){
// image file re-organization, additional sizes
console.log(" running migration v3");
let pins = db.prepare(`SELECT * FROM pins`).all();
if ( require("fs").existsSync(`${IMAGE_PATH}`) ){
let userdirs = await fs.readdir(IMAGE_PATH);
console.log(" migrating images");
for ( let i = 0; i < userdirs.length; ++i ){
if ( require("fs").existsSync(`${IMAGE_PATH}/${userdirs[i]}/images/originals`) ){
await fs.rename(`${IMAGE_PATH}/${userdirs[i]}/images/originals`, `${IMAGE_PATH}/${userdirs[i]}/images/o`);
}
if ( require("fs").existsSync(`${IMAGE_PATH}/${userdirs[i]}/images/thumbnails`) ){
await fs.rename(`${IMAGE_PATH}/${userdirs[i]}/images/thumbnails`, `${IMAGE_PATH}/${userdirs[i]}/images/400`);
}
}
}
if ( pins.length > 0 ){
console.log(" generating additional image sizes...");
}
for ( let i = 0; i < pins.length; ++i ){
let pin = pins[i];
let originalImagePath = getImagePath(pin.userId, pin.id, 'o');
for ( let i = 0; i < ADDITIONAL_IMAGE_SIZES.length; ++i ){
let size = ADDITIONAL_IMAGE_SIZES[i];
let img = await sharp(originalImagePath.file);
let resizedImg = await img.resize({width: size, height: size, fit: 'inside'})
let buf = await resizedImg.toBuffer();
let imgPath = getImagePath(pin.userId, pin.id, size);
await fs.mkdir(imgPath.dir, {recursive:true});
await fs.writeFile(imgPath.file, buf);
console.log(` saved additional size ${size} for pin#${pin.id} to: ${imgPath.file}`);
}
}
if ( pins.length > 0 ){
console.log(" finished generating addditional image sizes");
}
db.prepare(`
INSERT INTO migrations (id, createDate) VALUES ( @id, @createDate )
`).run({id:3, createDate: new Date().toISOString()});
schemaVersion = 3;
}
console.log(`database ready - schema version v${schemaVersion}`);
console.log('');
}
/**
* Downloads the image, converts it to JPG, and creates the thumbnail size so that standard dimensions can be taken
* @param {string} imageUrl
*/
async function downloadImage(imageUrl){
let res = await fetch(imageUrl);
@ -614,11 +793,15 @@ async function downloadImage(imageUrl){
let buffer = await res.buffer();
let original = sharp(buffer);
let originalMetadata = await original.metadata();
let originalBuffer = await original.toFormat("jpg").toBuffer();
return await processImage(buffer);
}
let thumbnail = await original.resize({ width: 400, height: 400, fit: 'inside' });
async function processImage(buffer){
let original = sharp(buffer);
let originalBuffer = await original.toFormat("jpg").toBuffer();
let originalMetadata = await original.metadata();
let thumbnail = await original.resize({ width: THUMBNAIL_IMAGE_SIZE, height: THUMBNAIL_IMAGE_SIZE, fit: 'inside' });
let thumbnailBuffer = await thumbnail.toBuffer();
let thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
@ -636,17 +819,43 @@ async function downloadImage(imageUrl){
}
}
// takes the response from downloadImage, creates ADDITIONAL_IMAGE_SIZE and writes all files to disk
async function saveImage(userId, pinId, image){
let originalImagePath = getImagePath(userId, pinId, 'o');
await fs.mkdir(originalImagePath.dir, {recursive: true});
await fs.writeFile(originalImagePath.file, image.original.buffer);
console.log(`saved original to: ${originalImagePath.file}`);
let thumbnailImagePath = getImagePath(userId, pinId, '400');
await fs.mkdir(thumbnailImagePath.dir, {recursive: true});
await fs.writeFile(thumbnailImagePath.file, image.thumbnail.buffer);
console.log(`saved thumbnail to: ${thumbnailImagePath.file}`);
// this will enlarge images if necessary, as the lanczos3 resize algorithm will create better looking enlargements than the browser will
for ( let i = 0; i < ADDITIONAL_IMAGE_SIZES.length; ++i ){
let size = ADDITIONAL_IMAGE_SIZES[i];
let img = await sharp(image.original.buffer);
let resizedImg = await img.resize({width: size, height: size, fit: 'inside'})
let buf = await resizedImg.toBuffer();
let imgPath = getImagePath(userId, pinId, size);
await fs.mkdir(imgPath.dir, {recursive:true});
await fs.writeFile(imgPath.file, buf);
console.log(`saved additional size ${size} to: ${imgPath.file}`);
}
function getOriginalImagePath(userId, pinId){
let paddedId = pinId.toString().padStart(12, '0');
let dir = `${IMAGE_PATH}/${userId}/images/originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return {dir: dir, file: file};
}
function getThumbnailImagePath(userId, pinId){
function getImagePath(userId, pinId, size){
let paddedId = pinId.toString().padStart(12, '0');
let dir = `${IMAGE_PATH}/${userId}/images/thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let dir = `${IMAGE_PATH}/${userId}/images/${size}/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return {dir: dir, file: file};
return { dir: dir, file: file};
}
})();

View file

@ -20,6 +20,8 @@
<link rel="stylesheet" href="client.css" />
</head>
<body>
<div id="ilen"></div>
<div id="app"></div>
<script src="reef.min.js"></script>
@ -28,6 +30,9 @@
<script src="reef-databind.js"></script>
<script>
document.getElementById("ilen").innerHTML = window.location.hash.length;
Reef.debug(true);
app.addSetter("render", (data) => {
@ -163,6 +168,7 @@ app.addSetter('addPinModal.save', async (data) => {
if ( res.status == 200 ){
window.localStorage.addPinLastBoardId = boardId;
data.done = true;
window.close();
}
@ -187,7 +193,8 @@ const store = new Reef.Store({
description: "",
saveInProgress: false
},
initialized: false
initialized: false,
done: false
},
getters: app.getGetters(),
setters: app.getSetters()
@ -205,6 +212,7 @@ const appComponent = new Reef("#app", {
</div>`;
}
//
let imagePlaceholder = 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22300%22%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%22300%22%20height%3D%22300%22%20style%3D%22fill%3A%23dedede%3B%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20font-size%3D%2218%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20font-family%3D%22monospace%2C%20sans-serif%22%20fill%3D%22%23555555%22%3Eimage%3C%2Ftext%3E%3C%2Fsvg%3E';
let options = "";
@ -225,12 +233,25 @@ const appComponent = new Reef("#app", {
}
return /*html*/`
<div id="doneModal">
<div class="modal ${data.done ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box has-text-centered" style="max-width: 90%; margin-left: auto; margin-right: auto;">
<!-- https://thenounproject.com/search/?q=done&i=587164 -->
<img style="width: 80px; height: 80px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjNGU4ODJiIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAwIDEwMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PGc+PHBhdGggZmlsbD0iIzRlODgyYiIgZD0iTTY5LjQzNyw0My4zMDNsLTI0LjEyLDI1Ljc1M2MtMC44MTcsMC44NzEtMS44NTEsMS4zMDctMi45OTUsMS4zMDdzLTIuMjMyLTAuNDM2LTIuOTk1LTEuMzA3ICAgIEwyNy4yNDEsNTYuMTUyYy0xLjUyNC0xLjYzMy0xLjQ3LTQuMTkyLDAuMjE4LTUuNzcxYzEuNjMzLTEuNTI1LDQuMTkyLTEuNDE2LDUuNzcxLDAuMjE4bDkuMDkzLDkuNjkxbDIxLjE4LTIyLjU5NSAgICBjMS41MjQtMS42MzMsNC4xMzgtMS43NDIsNS43NzEtMC4xNjNDNzAuOTA3LDM5LjA1Niw3MS4wMTYsNDEuNjE1LDY5LjQzNyw0My4zMDN6Ij48L3BhdGg+PHBhdGggZmlsbD0iIzRlODgyYiIgZD0iTTUuMDgyLDUwQzUuMDgyLDI1LjIyNywyNS4xNzIsNS4xMzYsNTAsNS4wODJDNzQuNzczLDUuMTM2LDk0LjkxOCwyNS4yMjcsOTQuOTE4LDUwICAgIGMwLDI0LjgyOC0yMC4xNDUsNDQuOTE4LTQ0LjkxOCw0NC45MThDMjUuMTcyLDk0LjkxOCw1LjA4Miw3NC44MjgsNS4wODIsNTB6IE0yMy45NzUsNzYuMDI1ICAgIEMzMC42NzIsODIuNjY4LDM5LjgxOSw4Ni43NTEsNTAsODYuNzUxYzEwLjEyNywwLDE5LjMyOC00LjA4MywyNS45NzEtMTAuNzI2YzYuNjQyLTYuNjk3LDEwLjc4LTE1Ljg0NCwxMC43OC0yNi4wMjUgICAgYzAtMTAuMTI3LTQuMTM4LTE5LjI3NC0xMC43OC0yNS45NzFDNjkuMzI4LDE3LjM4Nyw2MC4xMjcsMTMuMjQ5LDUwLDEzLjI0OWMtMTAuMTgxLDAtMTkuMzI4LDQuMTM4LTI2LjAyNSwxMC43OCAgICBDMTcuMzMyLDMwLjcyNiwxMy4yNDksMzkuODczLDEzLjI0OSw1MEMxMy4yNDksNjAuMTgxLDE3LjMzMiw2OS4zMjgsMjMuOTc1LDc2LjAyNXoiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPg==" />
</div>
</div>
</div>
</div>
</div>
<div id="addPinModal">
<div class="modal is-active">
<div class="modal ${!data.done ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add Pin ${data.addPinModal.boardId}</p>
<p class="modal-card-title">Add Pin ${window.location.hash.length}</p>
<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
</header>
<section class="modal-card-body">

View file

@ -311,7 +311,7 @@
object-fit: contain;
}
.supports-touch .lg-prev, .supports-touch .lg-next{
.is-touch .lg-prev, .is-touch .lg-next{
display: none;
}
@ -358,6 +358,44 @@
opacity: 1;
}
#lg-openOriginal {
/* https://thenounproject.com/search/?q=download&i=2120379 */
background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjRkZGRkZGIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzAgMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwIDMwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PHBhdGggZD0iTTI1LDI0SDVjLTAuNiwwLTEsMC40LTEsMXMwLjQsMSwxLDFoMjBjMC42LDAsMS0wLjQsMS0xUzI1LjYsMjQsMjUsMjR6Ij48L3BhdGg+PHBhdGggZD0iTTE0LjMsMjEuOWMwLjIsMC4yLDAuNSwwLjMsMC43LDAuM3MwLjUtMC4xLDAuNy0wLjNsNy4xLTcuMWMwLjQtMC40LDAuNC0xLDAtMS40cy0xLTAuNC0xLjQsMEwxNiwxOC44VjVjMC0wLjYtMC40LTEtMS0xICAgcy0xLDAuNC0xLDF2MTMuOGwtNS40LTUuNGMtMC40LTAuNC0xLTAuNC0xLjQsMHMtMC40LDEsMCwxLjRMMTQuMywyMS45eiI+PC9wYXRoPjwvZz48L3N2Zz4=");
background-repeat: no-repeat;
background-size: 20px 20px;
background-position: 16px 14px;
opacity: 0.7;
transition: opacity 0.2s linear;
}
#lg-openOriginal:hover {
opacity: 1;
}
#lg-iosShare {
/* https://thenounproject.com/search/?q=share&i=1058858 */
background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjRkZGRkZGIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT40LjE8L3RpdGxlPjxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPjxnIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI0ZGRkZGRiI+PGc+PHBhdGggZD0iTTI2LDcuODM3NTM4ODcgTDI2LDE1IEwzOC4wMDk4MjE0LDE1IEM0MC4yMTM1MzYyLDE1IDQyLDE2Ljc4OTAyNzUgNDIsMTguOTg4MTIwNiBMNDIsNDMuMDExODc5NCBDNDIsNDUuMjE0NDU3NiA0MC4yMTQ3NTQ0LDQ3IDM4LjAwOTgyMTQsNDcgTDkuOTkwMTc4NTksNDcgQzcuNzg2NDYzOCw0NyA2LDQ1LjIxMDk3MjUgNiw0My4wMTE4Nzk0IEw2LDE4Ljk4ODEyMDYgQzYsMTYuNzg1NTQyNCA3Ljc4NTI0NTYsMTUgOS45OTAxNzg1OSwxNSBMMjIsMTUgTDIyLDcuODMxMDU5MSBMMTkuNzQxMDc3NSwxMC4wODk5ODE2IEMxOC45NjcyMzE4LDEwLjg2MzgyNzMgMTcuNzEyMTg2MiwxMC44NjM0MzM2IDE2LjkyNTY5MjMsMTAuMDc2OTM5NyBDMTYuMTQ0NjQzNyw5LjI5NTg5MTA5IDE2LjE0OTI1NCw4LjAyNDk1MDg5IDE2LjkxMjY1MDQsNy4yNjE1NTQ0NyBMMjIuNTk1NTg4NSwxLjU3ODYxNjM4IEMyMi45ODAzNzI2LDEuMTkzODMyMjEgMjMuNDg0MTMwNSwxLjAwMDQ3NDA0IDIzLjk5MDEwMSwxIEwyNC4wMDUzNjYyLDEuMDA1OTcwNzYgQzI0LjUxMzI5MjgsMS4wMDcwNTQzMiAyNS4wMTgzNTI3LDEuMTk5MDM3MzQgMjUuMzk3OTMxOCwxLjU3ODYxNjM4IEwzMS4wODA4Njk5LDcuMjYxNTU0NDcgQzMxLjg1NDcxNTYsOC4wMzU0MDAyIDMxLjg1NDMyMTgsOS4yOTA0NDU3NiAzMS4wNjc4Mjc5LDEwLjA3NjkzOTcgQzMwLjI4Njc3OTMsMTAuODU3OTg4MyAyOS4wMTU4MzkyLDEwLjg1MzM3OCAyOC4yNTI0NDI3LDEwLjA4OTk4MTYgTDI2LDcuODM3NTM4ODcgWiBNMjIsMTkgTDEwLDE5IEwxMCw0MyBMMzgsNDMgTDM4LDE5IEwyNiwxOSBMMjYsMjcuMDAyOTk1MyBDMjYsMjguMTA1OTEwNiAyNS4xMTIyNzA0LDI5IDI0LDI5IEMyMi44OTU0MzA1LDI5IDIyLDI4LjEwNTAyMTEgMjIsMjcuMDAyOTk1MyBMMjIsMTkgWiBNMTYsMTUgTDE2LDE5IEwzMiwxOSBMMzIsMTUgTDE2LDE1IFogTTIyLDE1IEwyNiwxNSBMMjYsMTkgTDIyLDE5IEwyMiwxNSBaIj48L3BhdGg+PC9nPjwvZz48L2c+PC9zdmc+");
background-repeat: no-repeat;
background-size: 20px 20px;
background-position: 16px 13px;
opacity: 0.7;
transition: opacity 0.2s linear;
display: none;
}
#lg-iosShare:hover {
opacity: 1;
}
.is-ios #lg-openOriginal {
display: none;
}
.is-ios #lg-iosShare {
display: block;
}
.lg-control-hide {
display: none;
}

View file

@ -44,10 +44,19 @@ app.addSetter("brickwall.deletePin", async (data) => {
store.do('loader.hide');
});
var supportsTouch = 'ontouchstart' in window;
if ( supportsTouch ){
document.body.classList.add("supports-touch");
function openOriginal(el){
let data = store.data;
let index = getLightGalleryIndex();
let pin = data.board.pins[index];
// alert(pin.id);
let path = getImagePath(pin.id, 'o');
// alert(path);
window.location = "https://sktp.quikstorm.net/" + path;
}
let lightgalleryElement = document.getElementById("lightgallery");
let lightgalleryOpen = false;
@ -57,13 +66,49 @@ function openLightGallery(pinId){
let elements = [];
let index = 0;
let maxWidth = window.innerWidth * window.devicePixelRatio;
let maxHeight = window.innerHeight * window.devicePixelRatio;
for ( let i = 0; i < data.board.pins.length; ++i ){
elements.push({
src: getOriginalImagePath(data.board.pins[i].id),
subHtml: data.board.pins[i].description,
siteUrl: data.board.pins[i].siteUrl
});
let pin = data.board.pins[i];
let item = {};
item.subHtml = pin.description;
item.siteUrl = pin.siteUrl;
const THUMBNAIL_IMAGE_SIZE = 400;
const IMAGE_SIZES = [400,800,1280,1920,2560]; //TODO: make this dynamic to share the server-side setting
// couldn't get srcset and sizes to work right with a vertical bounding box, so we'll just push the right image for the screen size. This won't refresh on resize until closing & re-opening the lightgallery.
let maxSize = maxWidth;
let isPortrait = "n";
// portrait
if ( pin.originalHeight > pin.originalWidth ){
maxSize = maxHeight;
isPortrait = "y";
}
maxSize = maxSize * 0.74; // take an image 74% smaller than the physical pixel count, we have 10% borders + prefer the smaller size file if it will be enlarged < 10% by the browser (74% just bumps ipad pro portrait into the 2048 size)
let bestSize = -1;
for ( let s = 0; s < IMAGE_SIZES.length; ++s ){
if ( maxSize <= IMAGE_SIZES[s] ){
bestSize = IMAGE_SIZES[s];
break;
}
}
if ( bestSize < 0 ){
bestSize = 'o';
}
item.src = getImagePath(pin.id, bestSize);
item.originalUrl = getImagePath(pin.id, 'o');
elements.push(item);
if ( data.board.pins[i].id == pinId ){
index = i;
}
@ -83,6 +128,11 @@ function openLightGallery(pinId){
backdropDuration: 0 // disable animate in
};
// disable automatically hiding controls on touch devices, they can tap to hide.
if ( window.isTouch ){
options.hideBarsDelay = 0;
}
lightGallery(lightgalleryElement, options );
lightgalleryOpen = true;
}
@ -170,7 +220,7 @@ app.addComponent('brickwall', (store) => { return new Reef('#brickwall', {
let boardImage = null;
if ( board.titlePinId > 0 ){
boardImage = `<img class="thumb" src="${getThumbnailImagePath(board.titlePinId)}" />`;
boardImage = `<img class="thumb" src="${getImagePath(board.titlePinId, 400)}" />`;
} else {
boardImage = `<div class="board-brick-missing-thumbnail"><img class="thumb" src="${missingThumbnailSrc}" /></div>`;
}
@ -196,7 +246,7 @@ app.addComponent('brickwall', (store) => { return new Reef('#brickwall', {
return { height: pin.thumbnailHeight, template: /*html*/`
<div class="brick" >
<a data-pinid="${pin.id}" onclick="openLightGallery(${pin.id})" >
<img class="thumb" src="${getThumbnailImagePath(pin.id)}" width="${pin.thumbnailWidth}" height="${pin.thumbnailHeight}"/>
<img class="thumb" src="${getImagePath(pin.id, 400)}" width="${pin.thumbnailWidth}" height="${pin.thumbnailHeight}"/>
</a>
</div>
`};
@ -236,7 +286,7 @@ app.addComponent('brickwall', (store) => { return new Reef('#brickwall', {
for ( let c = 1; c < columns.length; ++c ){
if ( columns[c].height < shortestHeight ){
shortestIndex = c;
shortestHeight = c.height;
shortestHeight = columns[c].height;
}
}

View file

@ -137,7 +137,7 @@ app.addComponent('editPinModal', (store) => { return new Reef("#editPinModal", {
<div class="add-pin-flex">
<div class="add-pin-flex-left">
<img src="${data.editPinModal.pin ? getThumbnailImagePath(data.editPinModal.pin.id) : ''}" />
<img src="${data.editPinModal.pin ? getImagePath(data.editPinModal.pin.id, 400) : ''}" />
</div>
<div class="add-pin-flex-right">

View file

@ -3,8 +3,10 @@
<head>
<title>tinypin</title>
<meta charset="utf-8">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="tinypin">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="apple-touch-icon" sizes="180x180" href="pub/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="pub/icons/favicon-32x32.png">
@ -19,20 +21,23 @@
<link rel="stylesheet" href="pub/bulma-custom.css" />
<link rel="stylesheet" href="lightgallery/css/lightgallery.min.css" />
<link rel="stylesheet" href="client.css" />
<script src="lightgallery/js/lightgallery-custom.js"></script>
</head>
<body>
<div id="app"></div>
<div id="lightgallery"></div>
<script src="utils.js"></script>
<script src="lightgallery/js/lightgallery-custom.js"></script>
<script src="reef.min.js"></script>
<script src="reef-bootstrap.js"></script>
<script src="reef-databind.js"></script>
<script src="utils.js"></script>
<script src="components/navbar.js"></script>
<script src="components/brickwall.js"></script>
<!-- <script src="components/pinzoom.js"></script> -->

View file

@ -594,7 +594,7 @@
// tinypin -- add controls
this.outer.querySelector('.lg-toolbar').insertAdjacentHTML('beforeend', '<a id="lg-deletePin" class="lg-icon" data-onclick="brickwall.deletePin"></a><a id="lg-siteUrl" class="lg-icon" target="_blank"></a><a id="lg-edit" class="lg-icon" data-onclick="brickwall.editPin"></a>');
this.outer.querySelector('.lg-toolbar').insertAdjacentHTML('beforeend', '<a id="lg-deletePin" class="lg-icon" data-onclick="brickwall.deletePin"></a><a id="lg-siteUrl" class="lg-icon" target="_blank"></a><a id="lg-edit" class="lg-icon" data-onclick="brickwall.editPin"></a><a id="lg-iosShare" class="lg-icon"></a><a id="lg-openOriginal" class="lg-icon" download></a>');
if (this.s.download) {
this.outer.querySelector('.lg-toolbar').insertAdjacentHTML('beforeend', '<a id="lg-download" aria-label="Download" target="_blank" download class="lg-download lg-icon"></a>');
@ -1036,7 +1036,15 @@
} else {
siteUrlEl.classList.add("lg-control-hide");
}
let openOriginalEl = document.getElementById("lg-openOriginal");
openOriginalEl.setAttribute("href", _this.s.dynamicEl[index].originalUrl);
let iosShareEl = document.getElementById("lg-iosShare");
iosShareEl.setAttribute("href", "shortcuts://run-shortcut?name=Open%20In&input=" + encodeURIComponent("https://sktp.quikstorm.net/" + _this.s.dynamicEl[index].originalUrl));
// end tinypin
_lgUtils2.default.trigger(_this.el, 'onBeforeSlide', {
prevIndex: _prevIndex,
index: index,

File diff suppressed because one or more lines are too long

View file

@ -3,17 +3,18 @@ function getOriginalImagePath(pinId){
return "";
}
let paddedId = pinId.toString().padStart(12, '0');
let dir = `images/originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let dir = `images/o/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return file;
}
function getThumbnailImagePath(pinId){
function getImagePath(pinId, size){
if ( !pinId ){
return "";
}
let paddedId = pinId.toString().padStart(12, '0');
let dir = `images/thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let dir = `images/${size}/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return file;
}
@ -59,3 +60,27 @@ function getPinIndexById(id){
function getPinById(id){
return store.data.board.pins[getPinIndexById(id)];
}
// feature detection
if ( 'ontouchstart' in window ){
window.isTouch = true;
document.body.classList.add("is-touch");
}
if ([
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document) ){
window.iOS = true;
document.body.classList.add("is-ios");
} else {
window.iOS = false;
}

42
utils/regen-thumbs.js Normal file
View file

@ -0,0 +1,42 @@
const fs = require("fs").promises;
const sharp = require("sharp");
const betterSqlite3 = require('better-sqlite3');
const DB_PATH = "tinypin.db";
const IMAGE_PATH = "images";
const db = betterSqlite3(DB_PATH);
( async () => {
let pins = db.prepare("select * from pins").all();
for ( let i = 0; i < pins.length; ++i ){
let pin = pins[i];
let originalPath = getImagePath(pin.userId, pin.id, 'o');
let img = await sharp(originalPath.file);
let resizedImg = await img.resize({width: 400, height: 400, fit: 'inside'})
let buf = await resizedImg.toBuffer();
let thumbPath = getImagePath(pin.userId, pin.id, 't');
await fs.mkdir(thumbPath.dir, {recursive: true});
await fs.writeFile(thumbPath.file, buf);
}
function getImagePath(userId, pinId, size){
let paddedId = pinId.toString().padStart(12, '0');
let dir = `${IMAGE_PATH}/${userId}/images/${size}/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return { dir: dir, file: file};
}
})();