Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f43db59
feat: controller support as compatibility info
budak7273 Apr 6, 2025
9e24de2
fix: make mod 'info' header translatable
budak7273 Apr 11, 2025
8d2749e
feat: dotted-underline N/A linux client to convey it has a tooltip
budak7273 Apr 11, 2025
abef0ae
feat: display message when no versions of a mod exist
budak7273 Apr 11, 2025
288055a
fix: add Controller to zod schema.
budak7273 Apr 11, 2025
26826fd
feat: WIP mod network activity disclosure card
budak7273 Apr 11, 2025
27eeb7d
feat: log reason for failed websocket requests
budak7273 Apr 12, 2025
5daf347
feat: mod network disclosure edit. wrap mod edit sections in cards wh…
budak7273 Apr 12, 2025
c3638bc
docs: remove outdated mention of fenv tool
budak7273 Mar 31, 2026
f7f4b6e
feat: log node env when running dev
budak7273 Mar 31, 2026
3c9d525
feat: base integrated ai disclosures code
rhit-silkaijr Mar 17, 2026
4ece014
feat: ai disclosures working
rhit-silkaijr Mar 17, 2026
9e79077
Removing tabs and fixing button
rhit-silkaijr Mar 19, 2026
6a4baa2
feat: required disclosures + backend struct sync
rhit-silkaijr Mar 27, 2026
e07a258
feat: frontend updates for disclosure requirements
rhit-silkaijr Apr 6, 2026
46ab67e
feat: base integrated ai disclosures code
rhit-silkaijr Mar 17, 2026
1639e06
feat: ai disclosures working
rhit-silkaijr Mar 17, 2026
8e555d2
Removing tabs and fixing button
rhit-silkaijr Mar 19, 2026
e5544ac
feat: required disclosures + backend struct sync
rhit-silkaijr Mar 27, 2026
b861606
fix: make Network Disclosure dropdown options translatable, mark desc…
budak7273 Apr 8, 2026
8a83353
fix: fix format inconsistencies, refactor to remove duplicate element…
budak7273 Apr 8, 2026
a6ac7bc
refactor: translation key names
budak7273 Apr 8, 2026
82cd30f
feat: fixing ai disclosure formatting
rhit-silkaijr Apr 9, 2026
11f2f14
feat: adding network disclosure check to version upload
rhit-silkaijr Apr 14, 2026
fd9d5f2
feat: fix nullable and non-disclosure issues
rhit-silkaijr Apr 27, 2026
5191521
feat: optional canSubmit and localization
rhit-silkaijr Apr 27, 2026
18dbdb4
fix: translation key, do not localize console error
budak7273 Apr 28, 2026
c3c1031
chore: cleanup after rebase
budak7273 Apr 29, 2026
4b90a78
feat: localize version upload blocking messages. add edit button icon
budak7273 Apr 29, 2026
8a4e930
feat: validate network and ai disclosures locally first
budak7273 May 4, 2026
ddfaa72
fix: `keys` warning for AIUseDisclosureInfo
budak7273 May 8, 2026
0ee5dfd
refactor: remove `canSubmit` obsoleted by local felte/zod vaildation
budak7273 May 8, 2026
c62422b
fix: bizarre edge case of `[object Object]` validation error for ai d…
budak7273 May 8, 2026
e501d06
chore: add vscode extension suggestion for live commitlint visualization
budak7273 May 8, 2026
834cb96
refactor: ensure ctrl-f'ability of ai disclosure translation keys
budak7273 May 8, 2026
f3d2797
fix: fix validation edge case workaround again now that api field is …
budak7273 May 22, 2026
6b5f4bd
refactor: remove unused ai disclosure field from create mod
budak7273 May 23, 2026
d13cedf
feat: warn about version number reservation on delete
budak7273 May 24, 2026
60bf403
refactor: use union type
budak7273 May 24, 2026
48c0c0d
fix: address some ts warnings
budak7273 May 24, 2026
4fbb35e
refactor: switch compat descriptions to $t and handle undefined in th…
budak7273 May 24, 2026
e2b15b5
feat: intentional ordering of compatibility dropdown options
budak7273 May 24, 2026
b61a05f
feat: icons for disclosure types, frame on disclosure body text
budak7273 May 25, 2026
9be646d
refactor: address some ts warnings
budak7273 May 25, 2026
3ec5d34
fix: controller compat aria label, spacing
budak7273 May 25, 2026
a7b4ef7
fix: make gql `updateMod` invalidate graphcache (untested)
budak7273 May 25, 2026
b44d674
fix: show "try it" message for untested controller compatibility
budak7273 May 25, 2026
b8781b1
fix: actually log errors on compatibility edit fail
budak7273 May 25, 2026
a6c846d
refactor: use svelte conditional classes, cleanup
budak7273 May 25, 2026
f2e018b
refactor: move NoDisclosure enum value to just frontend
budak7273 May 25, 2026
7a7c621
refactor: rename `disclosure_string` to `message`
budak7273 May 25, 2026
263fbe2
fix: "allow" null ai disclosure to surface backend error instead of s…
budak7273 May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,5 @@ $RECYCLE.BIN/
/graphql.schema.*
/.svelte-kit
/.pnpm-store
.direnv
.direnv
vite.config.js.timestamp-*.mjs
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"csstools.postcss",
"vunguyentuan.vscode-postcss",
"github.copilot",
"eamodio.gitlens"
"eamodio.gitlens",
"joshbolduc.commitlint"
]
}
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ To do this:
3. Visit [the project's integration page](https://translate.ficsit.app/projects/2/integrate) to generate an API key (the "weapon" you select does not matter)
4. Replace the `PUBLIC_TOLGEE_API_KEY` in your new env file with the API key you generated
5. Use your new env name when running commands.
For example, run `fenv personal` to switch to it.
For example, use `NODE_ENV=personal` before commands to use it.

Using the in-context translation Screenshot feature also requires installing the _Tolgee Tools_ browser extension.

Expand Down Expand Up @@ -159,11 +159,11 @@ It executes several smaller scripts for you:
* GraphQL Code Generator with hot reload (graphql-codegen:watch)
* ESLint with hot reload (lint:dev)

Run the dev script via `bun dev`.
Run the dev script via `bun run dev`.
For example:

```shell
NODE_ENV=staging bun dev
NODE_ENV=staging bun run dev
```

The expected output is:
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"fenv",
"ficsit",
"gpgsign",
"ostring",
"pnpx",
"prismjs",
"SCIM",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"build:static": "rimraf .svelte build/static && vite build",
"check": "svelte-check --ignore build,.svelte,.svelte-kit",
"check:dev": "svelte-check --ignore build,.svelte,.svelte-kit --watch",
"dev": "conc -c 'auto' bun:graphql-codegen:watch bun:dev:serve bun:check:dev bun:lint:dev",
"log_env": "bun -e \"console.log('Current NODE_ENV:', process.env.NODE_ENV)\"",
"dev": "conc -c 'auto' bun:log_env bun:graphql-codegen:watch bun:dev:serve bun:check:dev bun:lint:dev",
"dev:serve": "vite dev --host",
"graphql-codegen": "graphql-codegen -r dotenv-flow/config --default-node-env=development --config codegen.yml",
"graphql-codegen:watch": "bun graphql-codegen --watch",
Expand Down
4 changes: 4 additions & 0 deletions src/gql/home/mods.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ query GetMods($offset: Int!, $limit: Int!, $search: String, $order: Order, $orde
note
state
}
Controller {
note
state
}
}
tags {
id
Expand Down
9 changes: 9 additions & 0 deletions src/gql/mods/edit_mod.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,20 @@ mutation EditMod($modId: ModID!, $mod: UpdateMod!) {
state
note
}
Controller {
state
note
}
}
tags {
id
name
description
}
network_use_disclosure
ai_use_disclosure {
disclosure_type
message
}
}
}
9 changes: 9 additions & 0 deletions src/gql/mods/mod.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,20 @@ query GetMod($mod: String!) {
note
state
}
Controller {
note
state
}
}
tags {
id
name
description
}
network_use_disclosure
ai_use_disclosure {
disclosure_type
message
}
}
}
4 changes: 4 additions & 0 deletions src/gql/users/user.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ query GetUser($user: UserID!) {
state
note
}
Controller {
state
note
}
}
tags {
id
Expand Down
208 changes: 156 additions & 52 deletions src/lib/components/mods/ModForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
import { createForm } from 'felte';
import { validator } from '@felte/validator-zod';
import { reporter, ValidationMessage } from '@felte/reporter-svelte';
import type { ModData } from '$lib/models/mods';
import { modSchema } from '$lib/models/mods';
import type { ModData, ModFormData } from '$lib/models/mods';
import {
AiDisclosureChoice,
AiDisclosureChoiceToDbType,
modFormSchema,
NetworkDisclosureState
} from '$lib/models/mods';
import { trimNonSchema } from '$lib/utils/forms';
import { markdown } from '$lib/utils/markdown';
import ModAuthor from '$lib/components/mods/ModAuthor.svelte';
import TagList from '$lib/components/utils/TagList.svelte';
import { CompatibilityState } from '$lib/generated';
import { CompatibilityState, ControllerCompatibilityState } from '$lib/generated';
import ModCompatibility from '$lib/components/mods/compatibility/ModCompatibilityEdit.svelte';
import { getTranslate } from '@tolgee/svelte';
import { SlideToggle } from '@skeletonlabs/skeleton';
import ModNetworkDisclosureEdit from './disclosure/ModNetworkDisclosureEdit.svelte';
import ModAiDisclosureEdit from './disclosure/ModAiDisclosureEdit.svelte';

export const { t } = getTranslate();

Expand All @@ -32,45 +39,100 @@
EXP: {
state: CompatibilityState.Works,
note: ''
},
Controller: {
state: ControllerCompatibilityState.Untested,
note: ''
}
}
};
export let submitText = $t('entry.create');

export let editing = false;

const { form, data } = createForm<ModData>({
let networkUseDisclosureDropdownChoice: NetworkDisclosureState = NetworkDisclosureState.Unspecified;

// Both the schema validator and validate() apply and merge their reported errors
const { form, data, errors } = createForm<ModFormData>({
initialValues: initialValues,
extend: [validator({ schema: modSchema }), reporter],
onSubmit: (submitted: ModData) => onSubmit(trimNonSchema(submitted, modSchema))
extend: [validator({ schema: modFormSchema }), reporter],
onSubmit: (submitted: ModFormData) => {
// Transform pending AI Disclosure data into the format the query wants
const choice = submitted.pending_ai_use_disclosure?.choice ?? AiDisclosureChoice.Unspecified;
let db_disclosure: ModData['ai_use_disclosure'] = null;
if (choice !== AiDisclosureChoice.Unspecified) {
db_disclosure = {
disclosure_type: AiDisclosureChoiceToDbType[choice],
message: submitted.pending_ai_use_disclosure!.message
};
}
delete submitted.pending_ai_use_disclosure;

// NewMod does not have a compatibility field
if (!editing) {
delete submitted.compatibility;
}

// trimNonSchema errors if ai_use_disclosure data is present, so it must be added after
const nonSchema: ModData = trimNonSchema(submitted, modFormSchema);
nonSchema.ai_use_disclosure = db_disclosure;
return onSubmit(nonSchema);
},
validate: (values) => {
if (
networkUseDisclosureDropdownChoice === NetworkDisclosureState.YesNetworkUsage &&
(values?.network_use_disclosure?.trim()?.length ?? 0) <= 0
) {
return {
network_use_disclosure: $t('mod.network_disclosure.developer.error.disclosure-required')
};
}
return {};
}
});

const _hasErrorContent = (key: string, value: unknown): boolean => {
if (value === null) {
return false;
} else if (key === 'authors') {
// Skip authors field since final submit gives it values unrelated to validation?
return false;
} else if (key === 'pending_ai_use_disclosure' && Array.isArray(value) && value.length === 0) {
// Stupid case where the custom error is eaten and turned into empty array
return true;
} else if (Array.isArray(value)) {
return value.some((item) => _hasErrorContent('', item));
} else if (typeof value === 'object') {
return Object.entries(value).some(([k, v]) => _hasErrorContent(k, v));
}
return true;
};

// Debug logging of form errors. Intentionally kept in case we run into some weird edge cases.
errors.subscribe((e) => {
if (Object.entries(e).some(([key, value]) => _hasErrorContent(key, value))) {
console.log('DEBUG: ModForm Errors', e);
}
});

let tags = $data.tags;
const computeTags = () => {
$data.tagIDs = tags.map((tag) => tag.id);
const computeTagIds = () => {
$data.tagIDs = tags!.map((tag) => tag.id);
};

$: if (tags) {
computeTags();
}

// The GQL type NewMod does not have a compatibility field.
// We remove the field from the data so that the GQL request is valid
$: {
if (!editing) {
delete $data.compatibility;
}
computeTagIds();
}

$: preview = ($data.full_description as string) || '';

const addAuthor = () => {
$data.authors.push({ role: 'editor', user_id: '', key: '' });
$data.authors!.push({ role: 'editor', user_id: '', key: '' });
$data.authors = $data.authors;
};

const removeAuthor = (i: number) => {
$data.authors.splice(i, 1);
$data.authors!.splice(i, 1);
$data.authors = $data.authors;
};

Expand Down Expand Up @@ -169,8 +231,9 @@
<span class="validation-message">{message || ''}</span>
</ValidationMessage>
</div>

{#if editing}
<div>
<div class="card p-4">
<SlideToggle
name="slider-label"
bind:checked={editCompatibility}
Expand All @@ -179,42 +242,83 @@
}}>
{$t('compatibility-info.edit')}
</SlideToggle>

{#if editCompatibility}
<ModCompatibility bind:compatibilityInfo={$data.compatibility} />
{/if}
</div>

{#if editCompatibility}
<ModCompatibility bind:compatibilityInfo={$data.compatibility} />
{/if}
<div class="card p-4">
<ModAiDisclosureEdit bind:ai_disclosure={$data.pending_ai_use_disclosure} />
<ValidationMessage for="pending_ai_use_disclosure" let:messages={message}>
<span class="validation-message"
>{(() => {
// Very bizarre data handling to work around zod/felte/something reporting error format improperly, in inconsistent formats (see mods.ts)
if (!message) {
// Case where there is no error
return '';
} else if ('message' in message) {
// Case where the custom error actually gets reported, but as an object
if (message.message === null) {
// When the user fixes the error, it changes to this state
return '';
}
return message?.message;
} else if (Array.isArray(message) && message.length === 0) {
// Case where nothing but `[]` is returned despite the custom error code executing
// (Intentionally not localized since the one in mods.ts isn't either)
return 'You must provide a description.';
} else {
// Backup in case something else comes out (who knows!?)
return 'Error displaying error message, raw value is: ' + JSON.stringify(message);
}
})()}</span>
</ValidationMessage>
</div>

<div class="grid grid-flow-row gap-2">
<div class="flex items-center">
<h4 class="mr-4">{$t('authors')}</h4>
<button class="variant-ghost-primary btn" type="button" on:click={addAuthor}>
<span>{$t('add')}</span>
</button>
</div>
{#each $data.authors as author, i}
<div class="flex items-end">
{#if $data.authors[i].user_id}
<div class="p-2">
<ModAuthor id={$data.authors[i].user_id} />
</div>
{/if}
<label class="label">
<span>User ID</span>
<input
type="text"
bind:value={$data.authors[i].user_id}
required
class="input p-2"
disabled={author.role === 'creator'} />
</label>
{#if author.role !== 'creator'}
<button class="variant-ghost-primary btn" type="button" on:click={() => removeAuthor(i)}>
<span>{$t('remove')}</span>
</button>
{/if}
<div class="card p-4">
<ModNetworkDisclosureEdit
bind:dropdownChoiceForValidation={networkUseDisclosureDropdownChoice}
bind:disclosure={$data.network_use_disclosure} />
<ValidationMessage for="network_use_disclosure" let:messages={message}>
<span class="validation-message">{message || ''}</span>
</ValidationMessage>
</div>

<div class="card p-4">
<div class="grid grid-flow-row gap-2">
<div class="flex items-center">
<h4 class="mr-4">{$t('authors')}</h4>
<button class="variant-ghost-primary btn" type="button" on:click={addAuthor}>
<span>{$t('add')}</span>
</button>
</div>
{/each}
{#if $data.authors !== undefined}
{#each $data.authors as author, i}
<div class="flex items-end">
{#if $data.authors[i].user_id}
<div class="p-2">
<ModAuthor id={$data.authors[i].user_id} />
</div>
{/if}
<label class="label">
<span>User ID</span>
<input
type="text"
bind:value={$data.authors[i].user_id}
required
class="input p-2"
disabled={author.role === 'creator'} />
</label>
{#if author.role !== 'creator'}
<button class="variant-ghost-primary btn" type="button" on:click={() => removeAuthor(i)}>
<span>{$t('remove')}</span>
</button>
{/if}
</div>
{/each}
{/if}
</div>
</div>
{/if}

Expand Down
Loading
Loading