feat(mod): add permissions and details

Close #112
This commit is contained in:
en 2025-01-22 17:47:38 +01:00
parent 99d7dbe89d
commit 5a6327fb73
5 changed files with 451 additions and 30 deletions

View File

@ -20,6 +20,9 @@ import {
DownloadUrl, DownloadUrl,
ModFormState, ModFormState,
ModPageLoaderResult, ModPageLoaderResult,
ModPermissions,
MODPERMISSIONS_CONF,
MODPERMISSIONS_DESC,
SubmitModActionResult SubmitModActionResult
} from '../types' } from '../types'
import { import {
@ -114,6 +117,13 @@ export const ModForm = () => {
[] []
) )
const handleRadioChange = useCallback((name: string, value: boolean) => {
setFormState((prevState) => ({
...prevState,
[name]: value
}))
}, [])
const addScreenshotUrl = useCallback(() => { const addScreenshotUrl = useCallback(() => {
setFormState((prevState) => ({ setFormState((prevState) => ({
...prevState, ...prevState,
@ -243,6 +253,17 @@ export const ModForm = () => {
[formState, isEditing, submit] [formState, isEditing, submit]
) )
const extraBoxRef = useRef<HTMLDivElement>(null)
const handleExtraBoxButtonClick = () => {
if (extraBoxRef.current) {
if (extraBoxRef.current.style.display === '') {
extraBoxRef.current.style.display = 'none'
} else {
extraBoxRef.current.style.display = ''
}
}
}
return ( return (
<form className='IBMSMSMBS_Write' onSubmit={handlePublish}> <form className='IBMSMSMBS_Write' onSubmit={handlePublish}>
<GameDropdown <GameDropdown
@ -479,6 +500,131 @@ export const ModForm = () => {
<InputError message={formErrors?.downloadUrls[0]} /> <InputError message={formErrors?.downloadUrls[0]} />
)} )}
</div> </div>
<div className='IBMSMSMBSSExtra'>
<button
className='btn btnMain IBMSMSMBSSExtraBtn'
type='button'
onClick={handleExtraBoxButtonClick}
>
Permissions &amp; Details
</button>
<div
className='IBMSMSMBSSExtraBox'
ref={extraBoxRef}
style={{
display: 'none'
}}
>
<p
className='labelDescriptionMain'
style={{ marginBottom: `10px`, textAlign: `center` }}
>
What permissions users have with your published mod/post
</p>
<div className='IBMSMSMBSSExtraBoxElementWrapper'>
{Object.keys(MODPERMISSIONS_CONF).map((k) => {
const permKey = k as keyof ModPermissions
const confKey = k as keyof typeof MODPERMISSIONS_CONF
const modPermission = MODPERMISSIONS_CONF[confKey]
const value = formState[permKey]
return (
<div className='IBMSMSMBSSExtraBoxElement' key={k}>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>{modPermission.header}</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<label
htmlFor={`${permKey}_true`}
className='IBMSMSMBSSExtraBoxElementColChoice'
>
<p>
{MODPERMISSIONS_DESC[`${permKey}_true`]}
<br />
</p>
<input
className='IBMSMSMBSSExtraBoxElementColChoiceRadio'
type='radio'
name={permKey}
id={`${permKey}_true`}
value={'true'}
checked={
typeof value !== 'undefined'
? value === true
: modPermission.default === true
}
onChange={(e) =>
handleRadioChange(
permKey,
e.currentTarget.value === 'true'
)
}
/>
<div className='IBMSMSMBSSExtraBoxElementColChoiceBox'></div>
</label>
<label
htmlFor={`${permKey}_false`}
className='IBMSMSMBSSExtraBoxElementColChoice'
>
<p>
{MODPERMISSIONS_DESC[`${permKey}_false`]}
<br />
</p>
<input
className='IBMSMSMBSSExtraBoxElementColChoiceRadio'
type='radio'
id={`${permKey}_false`}
value={'false'}
name={permKey}
checked={
typeof value !== 'undefined'
? value === false
: modPermission.default === false
}
onChange={(e) =>
handleRadioChange(
permKey,
e.currentTarget.value === 'true'
)
}
/>
<div className='IBMSMSMBSSExtraBoxElementColChoiceBox'></div>
</label>
</div>
</div>
)
})}
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Publisher Notes</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<textarea
className='inputMain'
value={formState.publisherNotes || ''}
onChange={(e) =>
handleInputChange('publisherNotes', e.currentTarget.value)
}
/>
</div>
</div>
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Extra Credits</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<textarea
className='inputMain'
value={formState.extraCredits || ''}
onChange={(e) =>
handleInputChange('extraCredits', e.currentTarget.value)
}
/>
</div>
</div>
</div>
</div>
</div>
<div className='IBMSMSMBS_WriteAction'> <div className='IBMSMSMBS_WriteAction'>
<button <button
className='btn btnMain' className='btn btnMain'

View File

@ -23,7 +23,15 @@ import '../../styles/styles.css'
import '../../styles/tabs.css' import '../../styles/tabs.css'
import '../../styles/tags.css' import '../../styles/tags.css'
import '../../styles/write.css' import '../../styles/write.css'
import { DownloadUrl, ModPageLoaderResult } from '../../types' import {
DownloadUrl,
ModDetails,
ModFormState,
ModPageLoaderResult,
ModPermissions,
MODPERMISSIONS_CONF,
MODPERMISSIONS_DESC
} from '../../types'
import { import {
capitalizeEachWord, capitalizeEachWord,
checkUrlForFile, checkUrlForFile,
@ -84,18 +92,7 @@ export const ModPage = () => {
<div className='IBMSMSplitMainBigSideSec'> <div className='IBMSMSplitMainBigSideSec'>
<Game /> <Game />
{postWarning && <PostWarnings type={postWarning} />} {postWarning && <PostWarnings type={postWarning} />}
<Body <Body {...mod} />
featuredImageUrl={mod.featuredImageUrl}
title={mod.title}
body={mod.body}
game={mod.game}
screenshotsUrls={mod.screenshotsUrls}
tags={mod.tags}
LTags={mod.LTags}
nsfw={mod.nsfw}
repost={mod.repost}
originalAuthor={mod.originalAuthor}
/>
<Interactions <Interactions
addressable={mod} addressable={mod}
commentCount={commentCount} commentCount={commentCount}
@ -390,18 +387,22 @@ const Game = () => {
) )
} }
type BodyProps = { type BodyProps = Pick<
featuredImageUrl: string ModDetails,
title: string | 'featuredImageUrl'
body: string | 'title'
game: string | 'body'
screenshotsUrls: string[] | 'game'
tags: string[] | 'screenshotsUrls'
LTags: string[] | 'tags'
nsfw: boolean | 'LTags'
repost: boolean | 'nsfw'
originalAuthor?: string | 'repost'
} | 'originalAuthor'
| keyof ModPermissions
| 'publisherNotes'
| 'extraCredits'
>
const Body = ({ const Body = ({
featuredImageUrl, featuredImageUrl,
@ -413,7 +414,15 @@ const Body = ({
LTags, LTags,
nsfw, nsfw,
repost, repost,
originalAuthor originalAuthor,
otherAssets,
uploadPermission,
modPermission,
convPermission,
assetUsePermission,
assetUseComPermission,
publisherNotes,
extraCredits
}: BodyProps) => { }: BodyProps) => {
const COLLAPSED_MAX_SIZE = 250 const COLLAPSED_MAX_SIZE = 250
const postBodyRef = useRef<HTMLDivElement>(null) const postBodyRef = useRef<HTMLDivElement>(null)
@ -485,6 +494,16 @@ const Body = ({
/> />
))} ))}
</div> </div>
<ExtraDetails
otherAssets={otherAssets}
uploadPermission={uploadPermission}
modPermission={modPermission}
convPermission={convPermission}
assetUsePermission={assetUsePermission}
assetUseComPermission={assetUseComPermission}
publisherNotes={publisherNotes}
extraCredits={extraCredits}
/>
<div className='IBMSMSMBSSTags'> <div className='IBMSMSMBSSTags'>
{nsfw && ( {nsfw && (
<div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW'> <div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW'>
@ -833,3 +852,122 @@ const DisplayModAuthorBlogs = () => {
</div> </div>
) )
} }
type ExtraDetailsProps = ModPermissions &
Pick<ModFormState, 'publisherNotes' | 'extraCredits'>
const ExtraDetails = ({
publisherNotes,
extraCredits,
...rest
}: ExtraDetailsProps) => {
const extraBoxRef = useRef<HTMLDivElement>(null)
if (
typeof publisherNotes === 'undefined' &&
typeof extraCredits === 'undefined' &&
Object.values(rest).every((v) => typeof v === 'undefined')
) {
return null
}
const handleClick = () => {
if (extraBoxRef.current) {
if (extraBoxRef.current.style.display === '') {
extraBoxRef.current.style.display = 'none'
} else {
extraBoxRef.current.style.display = ''
}
}
}
return (
<div className='IBMSMSMBSSExtra'>
<button
className='btn btnMain IBMSMSMBSSExtraBtn'
type='button'
onClick={handleClick}
>
Permissions &amp; Details
</button>
<div
className='IBMSMSMBSSExtraBox'
ref={extraBoxRef}
style={{
display: 'none'
}}
>
<div className='IBMSMSMBSSExtraBoxElementWrapper'>
{Object.keys(MODPERMISSIONS_CONF).map((k) => {
const permKey = k as keyof ModPermissions
const confKey = k as keyof typeof MODPERMISSIONS_CONF
const modPermission = MODPERMISSIONS_CONF[confKey]
const value = rest[permKey]
if (typeof value === 'undefined') return null
const text = MODPERMISSIONS_DESC[`${permKey}_${value}`]
return (
<div className='IBMSMSMBSSExtraBoxElement' key={k}>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>{modPermission.header}</p>
{value ? (
<div className='IBMSMSMBSSExtraBoxElementColMark IBMSMSMBSSExtraBoxElementColMarkGreen'>
<svg
className='IBMSMSMSSS_Author_Top_Icon'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM371.8 211.8C382.7 200.9 382.7 183.1 371.8 172.2C360.9 161.3 343.1 161.3 332.2 172.2L224 280.4L179.8 236.2C168.9 225.3 151.1 225.3 140.2 236.2C129.3 247.1 129.3 264.9 140.2 275.8L204.2 339.8C215.1 350.7 232.9 350.7 243.8 339.8L371.8 211.8z'></path>
</svg>
</div>
) : (
<div className='IBMSMSMBSSExtraBoxElementColMark IBMSMSMBSSExtraBoxElementColMarkRed'>
<svg
className='IBMSMSMSSS_Author_Top_Icon'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM175 208.1L222.1 255.1L175 303C165.7 312.4 165.7 327.6 175 336.1C184.4 346.3 199.6 346.3 208.1 336.1L255.1 289.9L303 336.1C312.4 346.3 327.6 346.3 336.1 336.1C346.3 327.6 346.3 312.4 336.1 303L289.9 255.1L336.1 208.1C346.3 199.6 346.3 184.4 336.1 175C327.6 165.7 312.4 165.7 303 175L255.1 222.1L208.1 175C199.6 165.7 184.4 165.7 175 175C165.7 184.4 165.7 199.6 175 208.1V208.1z'></path>
</svg>
</div>
)}
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<p>
{text}
<br />
</p>
</div>
</div>
)
})}
{typeof publisherNotes !== 'undefined' && publisherNotes !== '' && (
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Publisher Notes</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<p>{publisherNotes}</p>
</div>
</div>
)}
{typeof extraCredits !== 'undefined' && extraCredits !== '' && (
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Extra Credits</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<p>{extraCredits}</p>
</div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@ -8,6 +8,7 @@ import { store } from 'store'
import { import {
FormErrors, FormErrors,
ModFormState, ModFormState,
MODPERMISSIONS_CONF,
SubmitModActionResult, SubmitModActionResult,
TimeoutError TimeoutError
} from 'types' } from 'types'
@ -95,8 +96,41 @@ export const submitModRouteAction =
...formState.downloadUrls.map((downloadUrl) => ...formState.downloadUrls.map((downloadUrl) =>
JSON.stringify(downloadUrl) JSON.stringify(downloadUrl)
) )
],
[
'otherAssets',
formState.otherAssets?.toString() ??
MODPERMISSIONS_CONF.otherAssets.default.toString()
],
[
'uploadPermission',
formState.uploadPermission?.toString() ??
MODPERMISSIONS_CONF.uploadPermission.toString()
],
[
'modPermission',
formState.modPermission?.toString() ??
MODPERMISSIONS_CONF.modPermission.toString()
],
[
'convPermission',
formState.convPermission?.toString() ??
MODPERMISSIONS_CONF.convPermission.toString()
],
[
'assetUsePermission',
formState.assetUsePermission?.toString() ??
MODPERMISSIONS_CONF.assetUsePermission.toString()
],
[
'assetUseComPermission',
formState.assetUseComPermission?.toString() ??
MODPERMISSIONS_CONF.assetUseComPermission.toString()
],
['publisherNotes', formState.publisherNotes ?? ''],
['extraCredits', formState.extraCredits ?? '']
] ]
]
if (formState.repost && formState.originalAuthor) { if (formState.repost && formState.originalAuthor) {
tags.push(['originalAuthor', formState.originalAuthor]) tags.push(['originalAuthor', formState.originalAuthor])
} }

View File

@ -37,8 +37,29 @@ export interface ModFormState {
lTags: string[] lTags: string[]
downloadUrls: DownloadUrl[] downloadUrls: DownloadUrl[]
published_at: number published_at: number
// Permissions and details
otherAssets?: boolean
uploadPermission?: boolean
modPermission?: boolean
convPermission?: boolean
assetUsePermission?: boolean
assetUseComPermission?: boolean
publisherNotes?: string
extraCredits?: string
} }
// Permissions
export type ModPermissions = Pick<
ModFormState,
| 'otherAssets'
| 'uploadPermission'
| 'modPermission'
| 'convPermission'
| 'assetUsePermission'
| 'assetUseComPermission'
>
export interface DownloadUrl { export interface DownloadUrl {
url: string url: string
title?: string title?: string
@ -95,3 +116,61 @@ export interface FormErrors {
author?: string author?: string
originalAuthor?: string originalAuthor?: string
} }
type ModPermissionsKeys = keyof ModPermissions
type ModPermissionsOpts = 'true' | 'false'
type ModPermissionsKeysOpts = `${ModPermissionsKeys}_${ModPermissionsOpts}`
type ModPermissionsDesc = {
[k in ModPermissionsKeysOpts]: string
}
type ModPermissionsConf = {
[k in ModPermissionsKeys]: {
header: string
default: boolean
}
}
export const MODPERMISSIONS_CONF: ModPermissionsConf = {
otherAssets: {
header: `Others' Assets`,
default: true
},
uploadPermission: {
header: `Redistribution Permission`,
default: true
},
modPermission: {
header: `Modification Permission`,
default: true
},
convPermission: {
header: `Conversion Permission`,
default: true
},
assetUsePermission: {
header: `Asset Use Permission`,
default: true
},
assetUseComPermission: {
header: `Asset Use Permission for Commercial Mods`,
default: false
}
}
export const MODPERMISSIONS_DESC: ModPermissionsDesc = {
otherAssets_true: `All assets in this file are either owned by the publisher or sourced from free-to-use modder's resources.`,
otherAssets_false: `Not all assets in this file are owned by the publisher or sourced from free-to-use modder's resources; some assets may be.`,
uploadPermission_true: `You are allowed to upload this file to other sites, but you must give credit to me as the creator (unless indicated otherwise).`,
uploadPermission_false: `You are not allowed to upload this file to other sites without explicit permission.`,
modPermission_true: `You may modify my files and release bug fixes or enhancements, provided that you credit me as the original creator.`,
modPermission_false: `You are not allowed to convert this file for use with other games without explicit permission.`,
convPermission_true: `You are permitted to convert this file for use with other games, as long as you credit me as the creator.`,
convPermission_false: `You are not permitted to convert this file for use with other games without explicit permission.`,
assetUsePermission_true: `You may use the assets in this file without needing permission, provided you give me credit.`,
assetUsePermission_false: `You must obtain explicit permission to use the assets in this file.`,
assetUseComPermission_true: `You are allowed to use assets from this file in mods or files that are sold for money on Steam Workshop or other platforms.`,
assetUseComPermission_false: `You are prohibited from using assets from this file in any mods or files that are sold for money on Steam Workshop or other platforms, unless given explicit permission.`
}

View File

@ -2,7 +2,7 @@ import _ from 'lodash'
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { ModDetails, ModFormState } from '../types' import { ModDetails, ModFormState } from '../types'
import { getTagValue, getTagValues } from './nostr' import { getTagValue, getTagValues, getFirstTagValue as _getFirstTagValue } from './nostr'
/** /**
* Extracts and normalizes mod data from an event. * Extracts and normalizes mod data from an event.
@ -26,6 +26,14 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
return tagValue ? parseInt(tagValue[0], 10) : defaultValue return tagValue ? parseInt(tagValue[0], 10) : defaultValue
} }
// Optional mod values
const otherAssets = _getFirstTagValue(event, 'otherAssets')
const uploadPermission = _getFirstTagValue(event, 'uploadPermission')
const modPermission = _getFirstTagValue(event, 'modPermission')
const convPermission = _getFirstTagValue(event, 'convPermission')
const assetUsePermission = _getFirstTagValue(event, 'assetUsePermission')
const assetUseComPermission = _getFirstTagValue(event, 'assetUseComPermission')
return { return {
id: event.id, id: event.id,
dTag: getFirstTagValue('d'), dTag: getFirstTagValue('d'),
@ -52,7 +60,15 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
), ),
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) => downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
JSON.parse(item) JSON.parse(item)
) ),
otherAssets: otherAssets ? otherAssets === 'true' : undefined,
uploadPermission: uploadPermission ? uploadPermission === 'true' : undefined,
modPermission: modPermission ? modPermission === 'true' : undefined,
convPermission: convPermission ? convPermission === 'true' : undefined,
assetUsePermission: assetUsePermission ? assetUsePermission === 'true' : undefined,
assetUseComPermission: assetUseComPermission ? assetUseComPermission === 'true' : undefined,
publisherNotes: _getFirstTagValue(event, 'publisherNotes'),
extraCredits: _getFirstTagValue(event, 'extraCredits')
} }
} }
@ -147,7 +163,15 @@ export const initializeFormState = (
customNote: '', customNote: '',
mediaUrl: '' mediaUrl: ''
} }
] ],
otherAssets: existingModData?.otherAssets ?? true,
uploadPermission: existingModData?.uploadPermission ?? true,
modPermission: existingModData?.modPermission ?? true,
convPermission:existingModData?.convPermission ?? true,
assetUsePermission:existingModData?.assetUsePermission ?? true,
assetUseComPermission:existingModData?.assetUseComPermission ?? false,
publisherNotes:existingModData?.publisherNotes || '',
extraCredits:existingModData?.extraCredits || ''
}) })
export const MOD_DRAFT_CACHE_KEY = 'draft-mod' export const MOD_DRAFT_CACHE_KEY = 'draft-mod'