diff --git a/apps/juxtaposition-ui/src/images.ts b/apps/juxtaposition-ui/src/images.ts index b5a6433b..6a23a5ae 100644 --- a/apps/juxtaposition-ui/src/images.ts +++ b/apps/juxtaposition-ui/src/images.ts @@ -220,12 +220,14 @@ export type ScreenshotUrls = { }; export type UploadScreenshotOptions = { + buffer?: Buffer; blob: string; pid: number; postId: string; }; export async function uploadScreenshot(opts: UploadScreenshotOptions): Promise { - const screenshotBuf = Buffer.from(opts.blob.replace(/\0/g, '').trim(), 'base64'); + // try buffer, otherwise blob + const screenshotBuf = opts.buffer ?? Buffer.from(opts.blob.replace(/\0/g, '').trim(), 'base64'); const screenshots = processJpgScreenshot(screenshotBuf); if (screenshots === null) { return null; diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx index 0df7878e..8a84e77e 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx @@ -135,7 +135,8 @@ communitiesRouter.get('/:communityID/create', async function (req, res) { name: community.name, url: `/posts/new`, show: 'post', - shotMode + shotMode, + community }; res.jsxForDirectory({ ctr: , diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx index 4472059c..0f962b23 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx @@ -28,7 +28,8 @@ import type { PaintingUrls } from '@/images'; import type { PostPageViewProps } from '@/services/juxt-web/views/web/postPageView'; import type { HydratedSettingsDocument } from '@/models/settings'; import type { ContentSchema } from '@/models/content'; -const upload = multer({ dest: 'uploads/' }); +const storage = multer.memoryStorage(); +const upload = multer({ storage }); export const postsRouter = express.Router(); const postLimit = rateLimit({ @@ -136,7 +137,7 @@ postsRouter.post('/empathy', yeahLimit, async function (req, res) { await redisRemove(`${post.pid}_user_page_posts`); }); -postsRouter.post('/new', postLimit, upload.none(), async function (req, res) { +postsRouter.post('/new', postLimit, upload.fields([{ name: 'shot', maxCount: 1 }]), async function (req, res) { await newPost(req, res); }); @@ -242,7 +243,7 @@ postsRouter.delete('/:post_id', async function (req, res) { await redisRemove(`${post.pid}_user_page_posts`); }); -postsRouter.post('/:post_id/new', postLimit, upload.none(), async function (req, res) { +postsRouter.post('/:post_id/new', postLimit, upload.fields([{ name: 'shot', maxCount: 1 }]), async function (req, res) { await newPost(req, res); }); @@ -270,7 +271,8 @@ postsRouter.get('/:post_id/create', async function (req, res) { pid: parent.pid, url: `/posts/${parent.id}/new`, show: 'post', - shotMode + shotMode, + community }; res.jsxForDirectory({ ctr: , @@ -335,7 +337,7 @@ postsRouter.post('/:post_id/report', upload.none(), async function (req, res) { }); async function newPost(req: Request, res: Response): Promise { - const { params, body, auth } = parseReq(req, { + const { params, body, files, auth } = parseReq(req, { params: z.object({ post_id: z.string().optional() }), @@ -350,7 +352,8 @@ async function newPost(req: Request, res: Response): Promise { spoiler: z.stringbool().default(false), is_app_jumpable: z.stringbool().default(false), language_id: z.coerce.number().optional() - }) + }), + files: ['shot'] }); const userSettings = await database.getUserSettings(auth().pid); @@ -370,7 +373,7 @@ async function newPost(req: Request, res: Response): Promise { } } } - if (params.post_id && (body.body === '' && body.painting === '' && body.screenshot === '')) { + if (params.post_id && (body.body === '' && body.painting === '' && body.screenshot === '' && files.shot.length == 0)) { res.status(422); return res.redirect('/posts/' + req.params.post_id.toString()); } @@ -404,8 +407,10 @@ async function newPost(req: Request, res: Response): Promise { } } let screenshots = null; - if (body.screenshot && getShotMode(community, auth().paramPackData) !== 'block') { + if ((body.screenshot || files.shot.length === 1) && + getShotMode(community, auth().paramPackData) !== 'block') { screenshots = await uploadScreenshot({ + buffer: files.shot[0]?.buffer, blob: body.screenshot, pid: auth().pid, postId diff --git a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/communityView.tsx b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/communityView.tsx index 994e9a96..ad714788 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/communityView.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/communityView.tsx @@ -76,15 +76,19 @@ export function CtrCommunityView(props: CommunityViewProps): ReactNode { ) : null} diff --git a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/newPostView.tsx b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/newPostView.tsx index 55f2b80b..40095c0f 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/newPostView.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/newPostView.tsx @@ -41,17 +41,17 @@ export function CtrNewPostView(props: NewPostViewProps): ReactNode { const url = useUrl(); const user = useUser(); const cache = useCache(); - const { ctrBanner, ctrLegacy } = props; + const { bannerUrl, legacy } = props.community ? url.ctrHeader(props.community) : {}; const name = props.name ?? cache.getUserName(props.pid ?? 0); return (
-
+
@@ -93,7 +93,18 @@ export function CtrNewPostView(props: NewPostViewProps): ReactNode { {props.shotMode !== 'block' ? ( -
Screenshots are not ready yet. Check back soon!
+
+ +
+ + +
+
+ +
+
+ + ) : null } diff --git a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/notificationListView.tsx b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/notificationListView.tsx index 9580fc5e..9e37503a 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/notificationListView.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/notificationListView.tsx @@ -31,7 +31,7 @@ function CtrNotificationItem(props: NotificationItemProps): ReactNode {

- +

- +

{notif.text} diff --git a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/userPageView.tsx b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/userPageView.tsx index cf1f431d..33f9ff91 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/userPageView.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/views/ctr/userPageView.tsx @@ -73,8 +73,8 @@ export function CtrUserPageView(props: UserPageViewProps): ReactNode { {isSelf ? : null} { canViewUser && !isSelf ? ( - ) : null} diff --git a/apps/juxtaposition-ui/src/services/juxt-web/views/web/newPostView.tsx b/apps/juxtaposition-ui/src/services/juxt-web/views/web/newPostView.tsx index 26eb5653..2278d1db 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/views/web/newPostView.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/views/web/newPostView.tsx @@ -1,8 +1,9 @@ import { T } from '@/services/juxt-web/views/common/components/T'; import { useUrl } from '@/services/juxt-web/views/common/hooks/useUrl'; import { useUser } from '@/services/juxt-web/views/common/hooks/useUser'; +import type { InferSchemaType } from 'mongoose'; import type { ReactNode } from 'react'; -import type { CommunityShotMode } from '@/models/communities'; +import type { CommunitySchema, CommunityShotMode } from '@/models/communities'; const empathies = [ { @@ -51,11 +52,10 @@ export type NewPostViewProps = { pid?: number; url: string; show: string; + // must provide messagePid OR community messagePid?: number; + community?: InferSchemaType; shotMode: CommunityShotMode; - // ctr only - ctrBanner?: string; - ctrLegacy?: boolean; }; export function WebNewPostView(props: NewPostViewProps): ReactNode { diff --git a/apps/juxtaposition-ui/webfiles/ctr/css/new-post-view.css b/apps/juxtaposition-ui/webfiles/ctr/css/new-post-view.css index 0358b9f2..36c50713 100644 --- a/apps/juxtaposition-ui/webfiles/ctr/css/new-post-view.css +++ b/apps/juxtaposition-ui/webfiles/ctr/css/new-post-view.css @@ -97,8 +97,83 @@ height: 100%; } - #shot-msg { - padding: 5px; + #shot-preview { + float: right; + width: 200px; + height: 120px; + + background-color: lightgray; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + border: 0; + } + + .shot-picker { + display: inline-block; + padding: 4px; + margin: 5px 14px; + border: 1px solid gray; + + position: relative; + border-radius: 2px; + + input { + appearance: none; + display: block; + padding: 0; + margin: 0; + cursor: pointer; + } + + .shot { + margin: auto; + border: 1px solid white; + border-radius: 2px; + + /* background-image set via script */ + background-size: contain; + + height: 50px; /* 48 + border */ + &.top { + /* 400x240px -> 80px + border */ + width: 82px; + } + &.btm { + /* 320x240px -> 64 + border*/ + width: 66px; + } + &:checked { + border: 1px solid #6b3cb5; + } + } + + #shot-clear { + width: 17px; + height: 17px; + position: absolute; + bottom: -4px; /* ^^; */ + right: -9px; + + border-radius: 2px; + border: 1px solid gray; + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#dfdfdf)) 0 0; + input { + width: 100%; + height: 100%; + } + } + } + + /* Display: none makes it stop taking focus.. so instead just kinda hide it */ + input[type="file"] { + position: absolute; + top: 0px; + opacity: 0; + /* Small enough to fit */ + width: 10px; + height: 10px; + pointer-events: none; } #memo-img-input { diff --git a/apps/juxtaposition-ui/webfiles/ctr/images/dummy-shot-bottom.jpg b/apps/juxtaposition-ui/webfiles/ctr/images/dummy-shot-bottom.jpg new file mode 100644 index 00000000..4d58d0bd Binary files /dev/null and b/apps/juxtaposition-ui/webfiles/ctr/images/dummy-shot-bottom.jpg differ diff --git a/apps/juxtaposition-ui/webfiles/ctr/images/dummy-shot-top.jpg b/apps/juxtaposition-ui/webfiles/ctr/images/dummy-shot-top.jpg new file mode 100644 index 00000000..8e51708b Binary files /dev/null and b/apps/juxtaposition-ui/webfiles/ctr/images/dummy-shot-top.jpg differ diff --git a/apps/juxtaposition-ui/webfiles/ctr/images/sprites/clear.png b/apps/juxtaposition-ui/webfiles/ctr/images/sprites/clear.png new file mode 100644 index 00000000..2fe83ade Binary files /dev/null and b/apps/juxtaposition-ui/webfiles/ctr/images/sprites/clear.png differ diff --git a/apps/juxtaposition-ui/webfiles/ctr/js/debug.js b/apps/juxtaposition-ui/webfiles/ctr/js/debug.js index 241e42c0..2f69fcf7 100644 --- a/apps/juxtaposition-ui/webfiles/ctr/js/debug.js +++ b/apps/juxtaposition-ui/webfiles/ctr/js/debug.js @@ -550,9 +550,9 @@ if (typeof cave === 'undefined') { console.log('cave.getPath(' + key + ')'); var value = localStorage.getItem(key); if (value == 0) { - return '/img/dummy-image/screenshot-dummy-3ds-low.jpeg'; + return '/images/dummy-shot-bottom.jpg'; } else { - return '/img/dummy-image/screenshot-dummy-3ds-upper.jpeg'; + return '/images/dummy-shot-top.jpg'; } }, /** diff --git a/apps/juxtaposition-ui/webfiles/ctr/js/juxt.js b/apps/juxtaposition-ui/webfiles/ctr/js/juxt.js index 415ebc5f..f96f216b 100644 --- a/apps/juxtaposition-ui/webfiles/ctr/js/juxt.js +++ b/apps/juxtaposition-ui/webfiles/ctr/js/juxt.js @@ -3,10 +3,9 @@ import './polyfills'; import { initCheckboxes } from './controls/checkbox'; import { initClientTabs } from './controls/ctabs'; import { initNewPostView } from './new-post-view'; -import { pjaxBack, pjaxCanGoBack, pjaxHistory, pjaxInit, pjaxLoadUrl, pjaxRefresh } from './pjax'; +import { pjaxBack, pjaxCanGoBack, pjaxInit, pjaxLoadUrl, pjaxRefresh } from './pjax'; import { initPostPageView, initYeahButton } from './post'; import { initToolbarConfigs } from './toolbar'; -import { classList } from './util'; import { GET, POST } from './xhr'; setInterval(checkForUpdates, 30000); @@ -47,6 +46,7 @@ function initMorePosts() { el.parentElement.outerHTML = response; initPosts(); initMorePosts(); + pjaxRefresh(); } else { el.parentElement.outerHTML = ''; } @@ -76,13 +76,11 @@ function initSpoilers() { for (var i = 0; i < els.length; i++) { els[i].addEventListener('click', function (e) { var el = e.currentTarget; - classList.remove( - document.getElementById('post-' + el.getAttribute('data-post-id')), + var target = document.getElementById('post-' + el.getAttribute('data-post-id')); + target.classList.remove( 'spoiler' ); - document.getElementById( - 'spoiler-' + el.getAttribute('data-post-id') - ).outerHTML = ''; + target.outerHTML = ''; cave.snd_playSe('SE_OLV_OK'); }); } @@ -102,19 +100,17 @@ function initTabs() { var child = el.children[0]; for (var i = 0; i < els.length; i++) { - if (classList.contains(els[i], 'selected')) { - classList.remove(els[i], 'selected'); - } + els[i].classList.remove('selected'); } - classList.add(el, 'selected'); + el.classList.add('selected'); GET(child.getAttribute('href') + '?pjax=true', function a(data) { var response = data.responseText; if (response && data.status === 200) { document.getElementsByClassName('tab-body')[0].innerHTML = response; - pjaxHistory.push(child.href); initPosts(); initMorePosts(); + pjaxRefresh(); cave.transition_end(); } }); @@ -165,14 +161,13 @@ function checkForUpdates() { function follow(el) { var id = el.getAttribute('data-community-id'); var count = document.getElementById('followers'); + var sprite = el.querySelector('.sprite.sp-yeah'); el.disabled = true; var params = 'id=' + id; - if (classList.contains(el, 'selected')) { - classList.remove(el, 'selected'); - cave.snd_playSe('SE_OLV_CANCEL'); - } else { - classList.add(el, 'selected'); + if (sprite.classList.toggle('selected')) { cave.snd_playSe('SE_OLV_MII_ADD'); + } else { + cave.snd_playSe('SE_OLV_CANCEL'); } POST(el.getAttribute('data-url'), params, function a(data) { diff --git a/apps/juxtaposition-ui/webfiles/ctr/js/new-post-view.ts b/apps/juxtaposition-ui/webfiles/ctr/js/new-post-view.ts index 93a80aa5..22b8728c 100644 --- a/apps/juxtaposition-ui/webfiles/ctr/js/new-post-view.ts +++ b/apps/juxtaposition-ui/webfiles/ctr/js/new-post-view.ts @@ -27,13 +27,69 @@ export function initNewPostView(): void { memo_image.addEventListener('click', delayedMemo); ctabOnShown(ctab, 'painting', delayedMemo); + initScreenshotControl(page, ctab); +} + +function initScreenshotControl(page: Element, ctab: Element): void { // screenshots var shot_tab = page.querySelector('[data-shot-mode]') as HTMLElement | null; - if (shot_tab !== null) { - var shotMode = shot_tab.getAttribute('data-shot-mode')!; - // Hide on game's request - if (shotMode === 'allow' && !cave.capture_isEnabled()) { - shot_tab.style.display = 'none'; - } + if (shot_tab === null) { + return; } + var shotMode = shot_tab.getAttribute('data-shot-mode')!; + // Hide on game's request + if (shotMode === 'allow' && !cave.capture_isEnabled()) { + shot_tab.style.display = 'none'; + return; + } + + // input type file + var shotUpload = page.querySelector('[data-shot-upload]') as HTMLInputElement; + // radios + var shots = page.querySelectorAll('[data-shot]') as NodeListOf; + var shotClear = page.querySelector('[data-shot-clear]') as HTMLInputElement; + // preview + var shotPreview = page.querySelector('[data-shot-preview]') as HTMLImageElement; + + // top/bottom screen picker + function pickShot(e: Event): void { + var me = e.currentTarget as HTMLInputElement; + var lls = me.getAttribute('data-lls')!; + + shotPreview.style.backgroundImage = `url(${cave.lls_getPath(lls)})`; + shotUpload.setAttribute('lls', lls); + } + shots.forEach(s => s.addEventListener('change', pickShot)); + + // has to be enabled at submission + page.addEventListener('submit', () => { + // Fidget with disabled to keep it out of the D-pad navigation + shotUpload.disabled = false; + shotUpload.focus(); + shotUpload.click(); + shotUpload.blur(); + }); + + // reset/clear button + shotClear.addEventListener('change', () => { + shotPreview.style.backgroundImage = ''; + shotUpload.removeAttribute('lls'); + shotUpload.value = ''; + }); + + // actually populate the images after page load (laggy) + function shot(e: Event): void { + shots.forEach((s) => { + var num = parseInt(s.getAttribute('data-shot')!); + var lls = s.getAttribute('data-lls')!; + + cave.lls_setCaptureImage(lls, num); + s.style.backgroundImage = `url(${cave.lls_getPath(lls)})`; + }); + + // no need to call more than once per load + e.currentTarget?.removeEventListener(e.type, shot); + } + + ctabOnShown(ctab, 'shot', shot); } diff --git a/apps/juxtaposition-ui/webfiles/ctr/js/polyfills/Element.prototype.classList.js b/apps/juxtaposition-ui/webfiles/ctr/js/polyfills/Element.prototype.classList.js index 078da965..8ef96de7 100644 --- a/apps/juxtaposition-ui/webfiles/ctr/js/polyfills/Element.prototype.classList.js +++ b/apps/juxtaposition-ui/webfiles/ctr/js/polyfills/Element.prototype.classList.js @@ -1,5 +1,5 @@ // Element.prototype.classList by Remy Sharp -// updated by thednp +// updated by thednp, ashquarky var ClassLIST = function (elem) { var classArr = (elem.getAttribute('class') || '').trim().split(/\s+/) || []; @@ -23,8 +23,10 @@ var ClassLIST = function (elem) { this.toggle = function (classNAME) { if (this.contains(classNAME)) { this.remove(classNAME); + return false; } else { this.add(classNAME); + return true; } }; }; diff --git a/apps/juxtaposition-ui/webfiles/ctr/js/post.ts b/apps/juxtaposition-ui/webfiles/ctr/js/post.ts index 9c8adce5..ff4e844a 100644 --- a/apps/juxtaposition-ui/webfiles/ctr/js/post.ts +++ b/apps/juxtaposition-ui/webfiles/ctr/js/post.ts @@ -1,6 +1,5 @@ // Script for the post page view (postPageView.tsx) import { deletePostById, empathyPostById } from './api'; -import { classList } from './util'; function deletePost(this: HTMLElement, _e: Event): void { var id = this.getAttribute('data-button-delete-post'); @@ -50,7 +49,7 @@ function yeahPost(this: HTMLInputElement, _e: Event): void { var count = document.getElementById('count-' + id)!; this.disabled = true; - if (classList.toggle(sprite, 'selected')) { + if (sprite.classList.toggle('selected')) { count.innerText = inc(count.innerText, 1); // @ts-expect-error incorrect upstream types for SE label cave.snd_playSe('SE_OLV_MII_ADD'); diff --git a/apps/juxtaposition-ui/webfiles/ctr/js/util.ts b/apps/juxtaposition-ui/webfiles/ctr/js/util.ts deleted file mode 100644 index 782049b8..00000000 --- a/apps/juxtaposition-ui/webfiles/ctr/js/util.ts +++ /dev/null @@ -1,20 +0,0 @@ -export var classList = { - contains: function (el: Element, string: string): boolean { - return el.className.indexOf(string) !== -1; - }, - add: function (el: Element, string: string): void { - el.className += ' ' + string; - }, - remove: function (el: Element, string: string): void { - el.className = el.className.replace(string, ''); - }, - toggle: function (el: Element, string: string): boolean { - if (!this.contains(el, string)) { - this.add(el, string); - return true; - } else { - this.remove(el, string); - return false; - } - } -}; diff --git a/apps/miiverse-api/src/images.ts b/apps/miiverse-api/src/images.ts index f7cd1812..8fab8ff0 100644 --- a/apps/miiverse-api/src/images.ts +++ b/apps/miiverse-api/src/images.ts @@ -220,12 +220,14 @@ export type ScreenshotUrls = { }; export type UploadScreenshotOptions = { + buffer?: Buffer; blob: string; pid: number; postId: string; }; export async function uploadScreenshot(opts: UploadScreenshotOptions): Promise { - const screenshotBuf = Buffer.from(opts.blob.replace(/\0/g, '').trim(), 'base64'); + // try buffer, otherwise blob + const screenshotBuf = opts.buffer ?? Buffer.from(opts.blob.replace(/\0/g, '').trim(), 'base64'); const screenshots = processJpgScreenshot(screenshotBuf); if (screenshots === null) { return null;