Showing preview only (5,387K chars total). Download the full file or copy to clipboard to get everything.
Repository: stashapp/stash-box
Branch: master
Commit: 5e4bbf1cb9e3
Files: 752
Total size: 5.0 MB
Directory structure:
gitextract_6obqz5rx/
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── discussion---request-for-commentary--rfc-.md
│ │ └── feature_request.md
│ └── workflows/
│ ├── build.yml
│ ├── frontend-lint.yml
│ └── golangci-lint.yml
├── .gitignore
├── .golangci.yml
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README.md
├── cmd/
│ └── stash-box/
│ ├── init.go
│ └── main.go
├── docker/
│ ├── build/
│ │ └── x86_64/
│ │ ├── Dockerfile
│ │ ├── db/
│ │ │ └── initdb.sh
│ │ └── docker-compose.yml
│ ├── ci/
│ │ └── x86_64/
│ │ ├── Dockerfile
│ │ └── docker_push.sh
│ └── production/
│ ├── docker-compose.yml
│ └── postgres/
│ └── Dockerfile
├── frontend/
│ ├── .gitattributes
│ ├── .gitignore
│ ├── .nvmrc
│ ├── README.md
│ ├── biome.json
│ ├── codegen.yml
│ ├── embed.go
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── App.scss
│ │ ├── App.tsx
│ │ ├── Login.tsx
│ │ ├── Main.tsx
│ │ ├── components/
│ │ │ ├── amendableEditCard/
│ │ │ │ ├── AmendableChangeRow.tsx
│ │ │ │ ├── AmendableImageChangeRow.tsx
│ │ │ │ ├── AmendableLinkedChangeRow.tsx
│ │ │ │ ├── AmendableListChangeRow.tsx
│ │ │ │ ├── AmendableModifyEdit.tsx
│ │ │ │ ├── AmendableURLChangeRow.tsx
│ │ │ │ ├── AmendmentContext.tsx
│ │ │ │ └── index.ts
│ │ │ ├── changeRow/
│ │ │ │ ├── ChangeRow.tsx
│ │ │ │ └── index.ts
│ │ │ ├── checkboxSelect/
│ │ │ │ ├── CheckboxSelect.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── deleteButton/
│ │ │ │ ├── DeleteButton.tsx
│ │ │ │ └── index.ts
│ │ │ ├── editCard/
│ │ │ │ ├── AddComment.tsx
│ │ │ │ ├── EditCard.tsx
│ │ │ │ ├── EditComment.tsx
│ │ │ │ ├── EditExpiration.tsx
│ │ │ │ ├── EditHeader.tsx
│ │ │ │ ├── EditStatus.tsx
│ │ │ │ ├── ModifyEdit.tsx
│ │ │ │ ├── VoteBar.tsx
│ │ │ │ ├── Votes.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── renderEntity.tsx
│ │ │ │ └── styles.scss
│ │ │ ├── editImages/
│ │ │ │ ├── editImages.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── form/
│ │ │ │ ├── BodyModification.tsx
│ │ │ │ ├── EditNote.tsx
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── NavButtons.tsx
│ │ │ │ ├── NoteInput.tsx
│ │ │ │ ├── SubmitButtons.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── fragments/
│ │ │ │ ├── ErrorMessage.tsx
│ │ │ │ ├── Favorite.tsx
│ │ │ │ ├── GenderIcon.tsx
│ │ │ │ ├── Help.tsx
│ │ │ │ ├── Icon.tsx
│ │ │ │ ├── LoadingIndicator.tsx
│ │ │ │ ├── PerformerName.tsx
│ │ │ │ ├── SearchHint.tsx
│ │ │ │ ├── SearchInput.tsx
│ │ │ │ ├── SiteLink.tsx
│ │ │ │ ├── TagLink.tsx
│ │ │ │ ├── Thumbnail.tsx
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── image/
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── imageCarousel/
│ │ │ │ ├── ImageCarousel.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── imageChangeRow/
│ │ │ │ ├── ImageChangeRow.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── linkedChangeRow/
│ │ │ │ ├── LinkedChangeRow.tsx
│ │ │ │ └── index.ts
│ │ │ ├── list/
│ │ │ │ ├── EditList.tsx
│ │ │ │ ├── List.tsx
│ │ │ │ ├── SceneList.tsx
│ │ │ │ ├── TagList.tsx
│ │ │ │ ├── URLList.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── listChangeRow/
│ │ │ │ ├── ListChangeRow.tsx
│ │ │ │ └── index.ts
│ │ │ ├── modal/
│ │ │ │ ├── Modal.tsx
│ │ │ │ └── index.ts
│ │ │ ├── multiSelect/
│ │ │ │ ├── MultiSelect.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── pagination/
│ │ │ │ ├── Pagination.tsx
│ │ │ │ └── index.ts
│ │ │ ├── performerCard/
│ │ │ │ ├── PerformerCard.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── performerSelect/
│ │ │ │ ├── PerformerSelect.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── sceneCard/
│ │ │ │ ├── SceneCard.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── searchField/
│ │ │ │ ├── SearchField.tsx
│ │ │ │ ├── handleResult.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── studioSelect/
│ │ │ │ ├── StudioSelect.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── tagFilter/
│ │ │ │ ├── TagFilter.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── tagSelect/
│ │ │ │ ├── TagSelect.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── title/
│ │ │ │ ├── Title.tsx
│ │ │ │ └── index.ts
│ │ │ ├── urlChangeRow/
│ │ │ │ ├── URLChangeRow.tsx
│ │ │ │ └── index.ts
│ │ │ └── urlInput/
│ │ │ ├── index.ts
│ │ │ ├── styles.scss
│ │ │ └── urlInput.tsx
│ │ ├── constants/
│ │ │ ├── enums.ts
│ │ │ ├── index.ts
│ │ │ └── route.ts
│ │ ├── context.tsx
│ │ ├── graphql/
│ │ │ ├── fragments/
│ │ │ │ ├── CommentFragment.gql
│ │ │ │ ├── EditFragment.gql
│ │ │ │ ├── FingerprintFragment.gql
│ │ │ │ ├── ImageFragment.gql
│ │ │ │ ├── PerformerFragment.gql
│ │ │ │ ├── QuerySceneFragment.gql
│ │ │ │ ├── SceneFragment.gql
│ │ │ │ ├── ScenePerformerFragment.gql
│ │ │ │ ├── SearchPerformerFragment.gql
│ │ │ │ ├── StudioFragment.gql
│ │ │ │ ├── TagFragment.gql
│ │ │ │ └── URLFragment.gql
│ │ │ ├── index.ts
│ │ │ ├── mutations/
│ │ │ │ ├── ActivateNewUser.gql
│ │ │ │ ├── AddImage.gql
│ │ │ │ ├── AddScene.gql
│ │ │ │ ├── AddSite.gql
│ │ │ │ ├── AddStudio.gql
│ │ │ │ ├── AddTagCategory.gql
│ │ │ │ ├── AddUser.gql
│ │ │ │ ├── AmendEdit.gql
│ │ │ │ ├── ApplyEdit.gql
│ │ │ │ ├── CancelEdit.gql
│ │ │ │ ├── ChangePassword.gql
│ │ │ │ ├── ConfirmChangeEmail.gql
│ │ │ │ ├── DeleteDraft.gql
│ │ │ │ ├── DeleteEdit.gql
│ │ │ │ ├── DeleteFingerprintSubmissions.gql
│ │ │ │ ├── DeleteScene.gql
│ │ │ │ ├── DeleteSite.gql
│ │ │ │ ├── DeleteStudio.gql
│ │ │ │ ├── DeleteTagCategory.gql
│ │ │ │ ├── DeleteUser.gql
│ │ │ │ ├── EditComment.gql
│ │ │ │ ├── FavoritePerformer.gql
│ │ │ │ ├── FavoriteStudio.gql
│ │ │ │ ├── GenerateInviteCode.gql
│ │ │ │ ├── GrantInvite.gql
│ │ │ │ ├── MarkNotificationRead.gql
│ │ │ │ ├── MarkNotificationsRead.gql
│ │ │ │ ├── MoveFingerprintSubmissions.gql
│ │ │ │ ├── NewUser.gql
│ │ │ │ ├── PerformerEdit.gql
│ │ │ │ ├── PerformerEditUpdate.gql
│ │ │ │ ├── RegenerateAPIKey.gql
│ │ │ │ ├── RequestChangeEmail.gql
│ │ │ │ ├── RescindInviteCode.gql
│ │ │ │ ├── ResetPassword.gql
│ │ │ │ ├── RevokeInvite.gql
│ │ │ │ ├── SceneEdit.gql
│ │ │ │ ├── SceneEditUpdate.gql
│ │ │ │ ├── StudioEdit.gql
│ │ │ │ ├── StudioEditUpdate.gql
│ │ │ │ ├── TagEdit.gql
│ │ │ │ ├── TagEditUpdate.gql
│ │ │ │ ├── UnmatchFingerprint.gql
│ │ │ │ ├── UpdateNotificationSubscriptions.gql
│ │ │ │ ├── UpdateScene.gql
│ │ │ │ ├── UpdateSite.gql
│ │ │ │ ├── UpdateStudio.gql
│ │ │ │ ├── UpdateTagCategory.gql
│ │ │ │ ├── UpdateUser.gql
│ │ │ │ ├── ValidateChangeEmail.gql
│ │ │ │ ├── Vote.gql
│ │ │ │ └── index.ts
│ │ │ ├── queries/
│ │ │ │ ├── Categories.gql
│ │ │ │ ├── Category.gql
│ │ │ │ ├── Config.gql
│ │ │ │ ├── Draft.gql
│ │ │ │ ├── Drafts.gql
│ │ │ │ ├── Edit.gql
│ │ │ │ ├── EditUpdate.gql
│ │ │ │ ├── Edits.gql
│ │ │ │ ├── FullPerformer.gql
│ │ │ │ ├── Me.gql
│ │ │ │ ├── ModAudits.gql
│ │ │ │ ├── PendingEditsCount.gql
│ │ │ │ ├── Performer.gql
│ │ │ │ ├── Performers.gql
│ │ │ │ ├── PublicUser.gql
│ │ │ │ ├── QueryExistingPerformer.gql
│ │ │ │ ├── QueryExistingScene.gql
│ │ │ │ ├── QueryNotifications.gql
│ │ │ │ ├── Scene.gql
│ │ │ │ ├── ScenePairings.gql
│ │ │ │ ├── Scenes.gql
│ │ │ │ ├── ScenesWithFingerprints.gql
│ │ │ │ ├── ScenesWithoutCount.gql
│ │ │ │ ├── SearchAll.gql
│ │ │ │ ├── SearchPerformers.gql
│ │ │ │ ├── SearchScenes.gql
│ │ │ │ ├── SearchTags.gql
│ │ │ │ ├── Site.gql
│ │ │ │ ├── Sites.gql
│ │ │ │ ├── Studio.gql
│ │ │ │ ├── StudioPerformers.gql
│ │ │ │ ├── Studios.gql
│ │ │ │ ├── SubStudios.gql
│ │ │ │ ├── Tag.gql
│ │ │ │ ├── Tags.gql
│ │ │ │ ├── UnreadNotificationCount.gql
│ │ │ │ ├── User.gql
│ │ │ │ ├── Users.gql
│ │ │ │ ├── Version.gql
│ │ │ │ └── index.ts
│ │ │ ├── scalars.d.ts
│ │ │ └── types.ts
│ │ ├── hooks/
│ │ │ ├── index.ts
│ │ │ ├── toast.scss
│ │ │ ├── useAuth.tsx
│ │ │ ├── useBeforeUnload.ts
│ │ │ ├── useCurrentUser.tsx
│ │ │ ├── useEditFilter.tsx
│ │ │ ├── usePagination.ts
│ │ │ ├── useQueryParams.ts
│ │ │ └── useToast.tsx
│ │ ├── index.tsx
│ │ ├── modules.d.ts
│ │ ├── pages/
│ │ │ ├── activateUser/
│ │ │ │ ├── ActivateUser.tsx
│ │ │ │ └── index.ts
│ │ │ ├── audits/
│ │ │ │ ├── AmendmentAuditDetails.tsx
│ │ │ │ ├── AuditRow.tsx
│ │ │ │ ├── Audits.tsx
│ │ │ │ ├── DeleteAuditDetails.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── categories/
│ │ │ │ ├── Categories.tsx
│ │ │ │ ├── Category.tsx
│ │ │ │ ├── CategoryAdd.tsx
│ │ │ │ ├── CategoryEdit.tsx
│ │ │ │ ├── categoryForm/
│ │ │ │ │ ├── CategoryForm.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.tsx
│ │ │ ├── drafts/
│ │ │ │ ├── Draft.tsx
│ │ │ │ ├── Drafts.tsx
│ │ │ │ ├── PerformerDraft.tsx
│ │ │ │ ├── SceneDraft.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── parse.ts
│ │ │ ├── edits/
│ │ │ │ ├── Edit.tsx
│ │ │ │ ├── EditAmend.tsx
│ │ │ │ ├── EditAmendForm.tsx
│ │ │ │ ├── EditUpdate.tsx
│ │ │ │ ├── Edits.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── DeleteEditModal.tsx
│ │ │ │ │ └── UpdateCount.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── forgotPassword/
│ │ │ │ ├── ForgotPassword.tsx
│ │ │ │ └── index.ts
│ │ │ ├── home/
│ │ │ │ ├── Home.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.scss
│ │ │ ├── index.tsx
│ │ │ ├── notifications/
│ │ │ │ ├── CommentNotification.tsx
│ │ │ │ ├── EditNotification.tsx
│ │ │ │ ├── Notification.tsx
│ │ │ │ ├── Notifications.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── sceneNotification.tsx
│ │ │ │ ├── styles.scss
│ │ │ │ └── types.ts
│ │ │ ├── performers/
│ │ │ │ ├── Performer.tsx
│ │ │ │ ├── PerformerAdd.tsx
│ │ │ │ ├── PerformerDelete.tsx
│ │ │ │ ├── PerformerEdit.tsx
│ │ │ │ ├── PerformerEditUpdate.tsx
│ │ │ │ ├── PerformerMerge.tsx
│ │ │ │ ├── Performers.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── performerInfo.tsx
│ │ │ │ │ └── scenePairings.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── performerForm/
│ │ │ │ │ ├── ExistingPerformerAlert.tsx
│ │ │ │ │ ├── PerformerForm.tsx
│ │ │ │ │ ├── diff.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── schema.ts
│ │ │ │ │ └── types.ts
│ │ │ │ └── styles.scss
│ │ │ ├── registerUser/
│ │ │ │ ├── Register.tsx
│ │ │ │ └── index.ts
│ │ │ ├── resetPassword/
│ │ │ │ ├── ResetPassword.tsx
│ │ │ │ └── index.ts
│ │ │ ├── scenes/
│ │ │ │ ├── Scene.tsx
│ │ │ │ ├── SceneAdd.tsx
│ │ │ │ ├── SceneDelete.tsx
│ │ │ │ ├── SceneEdit.tsx
│ │ │ │ ├── SceneEditUpdate.tsx
│ │ │ │ ├── Scenes.tsx
│ │ │ │ ├── components/
│ │ │ │ │ └── fingerprints/
│ │ │ │ │ ├── DeleteFingerprintsModal.tsx
│ │ │ │ │ ├── FingerprintTable.tsx
│ │ │ │ │ ├── FingerprintTableHeader.tsx
│ │ │ │ │ ├── FingerprintTableRow.tsx
│ │ │ │ │ ├── MoveFingerprintsModal.tsx
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── useFingerprintOperations.ts
│ │ │ │ │ ├── useFingerprintSelection.ts
│ │ │ │ │ └── useFingerprintSort.ts
│ │ │ │ ├── index.tsx
│ │ │ │ ├── sceneForm/
│ │ │ │ │ ├── ExistingSceneAlert.tsx
│ │ │ │ │ ├── SceneForm.tsx
│ │ │ │ │ ├── diff.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── schema.ts
│ │ │ │ │ ├── styles.scss
│ │ │ │ │ └── types.ts
│ │ │ │ └── styles.scss
│ │ │ ├── search/
│ │ │ │ ├── GenderFacet.tsx
│ │ │ │ ├── PerformerCard.tsx
│ │ │ │ ├── SceneCard.tsx
│ │ │ │ ├── SearchAll.tsx
│ │ │ │ ├── SearchLayout.tsx
│ │ │ │ ├── SearchPerformersTab.tsx
│ │ │ │ ├── SearchScenesTab.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── search.scss
│ │ │ ├── sites/
│ │ │ │ ├── Site.tsx
│ │ │ │ ├── SiteAdd.tsx
│ │ │ │ ├── SiteEdit.tsx
│ │ │ │ ├── Sites.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── siteForm/
│ │ │ │ ├── SiteForm.tsx
│ │ │ │ └── index.ts
│ │ │ ├── studios/
│ │ │ │ ├── Studio.tsx
│ │ │ │ ├── StudioAdd.tsx
│ │ │ │ ├── StudioDelete.tsx
│ │ │ │ ├── StudioEdit.tsx
│ │ │ │ ├── StudioEditUpdate.tsx
│ │ │ │ ├── Studios.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── studioPerformers.tsx
│ │ │ │ │ ├── subStudioList.tsx
│ │ │ │ │ └── subStudioPreview.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── studioForm/
│ │ │ │ │ ├── StudioForm.tsx
│ │ │ │ │ ├── diff.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── schema.ts
│ │ │ │ │ └── types.ts
│ │ │ │ └── styles.scss
│ │ │ ├── tags/
│ │ │ │ ├── Tag.tsx
│ │ │ │ ├── TagAdd.tsx
│ │ │ │ ├── TagDelete.tsx
│ │ │ │ ├── TagEdit.tsx
│ │ │ │ ├── TagEditUpdate.tsx
│ │ │ │ ├── TagMerge.tsx
│ │ │ │ ├── Tags.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── tagForm/
│ │ │ │ ├── TagForm.tsx
│ │ │ │ ├── diff.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── schema.ts
│ │ │ │ └── types.ts
│ │ │ ├── users/
│ │ │ │ ├── GenerateInviteKeyModal.tsx
│ │ │ │ ├── User.tsx
│ │ │ │ ├── UserAdd.tsx
│ │ │ │ ├── UserConfirmChangeEmail.tsx
│ │ │ │ ├── UserEdit.tsx
│ │ │ │ ├── UserEditForm.tsx
│ │ │ │ ├── UserEdits.tsx
│ │ │ │ ├── UserFingerprints.tsx
│ │ │ │ ├── UserFingerprintsList/
│ │ │ │ │ ├── UserFingerprint.tsx
│ │ │ │ │ ├── UserFingerprintsList.tsx
│ │ │ │ │ ├── UserSceneLine.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── UserForm.tsx
│ │ │ │ ├── UserNotificationPreferences.tsx
│ │ │ │ ├── UserPassword.tsx
│ │ │ │ ├── UserPasswordForm.tsx
│ │ │ │ ├── UserValidateChangeEmail.tsx
│ │ │ │ ├── Users.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.scss
│ │ │ └── version/
│ │ │ ├── Version.tsx
│ │ │ └── index.ts
│ │ ├── styles/
│ │ │ └── theme.scss
│ │ └── utils/
│ │ ├── country.ts
│ │ ├── createClient.ts
│ │ ├── data.ts
│ │ ├── date.ts
│ │ ├── diff.ts
│ │ ├── edit.ts
│ │ ├── enum.ts
│ │ ├── general.ts
│ │ ├── index.ts
│ │ ├── intl.ts
│ │ ├── markdown.tsx
│ │ ├── route.ts
│ │ ├── transforms.ts
│ │ ├── url.ts
│ │ └── user.ts
│ ├── tsconfig.json
│ └── vite.config.mjs
├── go.mod
├── go.sum
├── gqlgen.yml
├── graphql/
│ └── schema/
│ ├── schema.graphql
│ └── types/
│ ├── config.graphql
│ ├── draft.graphql
│ ├── edit.graphql
│ ├── filter.graphql
│ ├── image.graphql
│ ├── misc.graphql
│ ├── mod_audit.graphql
│ ├── notifications.graphql
│ ├── performer.graphql
│ ├── scene.graphql
│ ├── site.graphql
│ ├── studio.graphql
│ ├── tag.graphql
│ ├── user.graphql
│ └── version.graphql
├── internal/
│ ├── api/
│ │ ├── context_keys.go
│ │ ├── directives.go
│ │ ├── draft_integration_test.go
│ │ ├── edit_amend_integration_test.go
│ │ ├── edit_delete_integration_test.go
│ │ ├── edit_integration_test.go
│ │ ├── field_test.go
│ │ ├── fingerprint_filter.go
│ │ ├── graphql_client_test.go
│ │ ├── integration_test.go
│ │ ├── loaders.go
│ │ ├── notification_integration_test.go
│ │ ├── performer_edit_integration_test.go
│ │ ├── performer_integration_test.go
│ │ ├── performer_resolver_integration_test.go
│ │ ├── resolver.go
│ │ ├── resolver_model_draft.go
│ │ ├── resolver_model_edit.go
│ │ ├── resolver_model_edit_comment.go
│ │ ├── resolver_model_edit_vote.go
│ │ ├── resolver_model_image.go
│ │ ├── resolver_model_notification.go
│ │ ├── resolver_model_performer.go
│ │ ├── resolver_model_performer_draft.go
│ │ ├── resolver_model_performer_edit.go
│ │ ├── resolver_model_scene.go
│ │ ├── resolver_model_scene_draft.go
│ │ ├── resolver_model_scene_edit.go
│ │ ├── resolver_model_site.go
│ │ ├── resolver_model_studio.go
│ │ ├── resolver_model_studio_edit.go
│ │ ├── resolver_model_tag.go
│ │ ├── resolver_model_tag_category.go
│ │ ├── resolver_model_tag_edit.go
│ │ ├── resolver_model_url.go
│ │ ├── resolver_model_user.go
│ │ ├── resolver_mutation_draft.go
│ │ ├── resolver_mutation_edit.go
│ │ ├── resolver_mutation_image.go
│ │ ├── resolver_mutation_notifications.go
│ │ ├── resolver_mutation_performer.go
│ │ ├── resolver_mutation_scene.go
│ │ ├── resolver_mutation_site.go
│ │ ├── resolver_mutation_studio.go
│ │ ├── resolver_mutation_tag.go
│ │ ├── resolver_mutation_tag_category.go
│ │ ├── resolver_mutation_user.go
│ │ ├── resolver_query_draft.go
│ │ ├── resolver_query_edit.go
│ │ ├── resolver_query_mod_audit.go
│ │ ├── resolver_query_notifications.go
│ │ ├── resolver_query_performer.go
│ │ ├── resolver_query_scene.go
│ │ ├── resolver_query_site.go
│ │ ├── resolver_query_studio.go
│ │ ├── resolver_query_tag.go
│ │ ├── resolver_query_tag_category.go
│ │ ├── resolver_query_user.go
│ │ ├── routes_image.go
│ │ ├── routes_root.go
│ │ ├── scene_edit_integration_test.go
│ │ ├── scene_integration_test.go
│ │ ├── search_integration_test.go
│ │ ├── server.go
│ │ ├── session.go
│ │ ├── site_integration_test.go
│ │ ├── studio_edit_integration_test.go
│ │ ├── studio_integration_test.go
│ │ ├── tag_category_integration_test.go
│ │ ├── tag_edit_integration_test.go
│ │ ├── tag_integration_test.go
│ │ ├── user_integration_test.go
│ │ └── utils.go
│ ├── auth/
│ │ └── authorization.go
│ ├── autocert/
│ │ └── autocert.go
│ ├── config/
│ │ ├── config.go
│ │ └── paths.go
│ ├── converter/
│ │ ├── converter.go
│ │ └── gen/
│ │ ├── extensions.go
│ │ ├── generated.go
│ │ └── interfaces.go
│ ├── cron/
│ │ └── cron.go
│ ├── database/
│ │ ├── database.go
│ │ ├── migrations/
│ │ │ └── postgres/
│ │ │ ├── 01_initial.down.sql
│ │ │ ├── 01_initial.up.sql
│ │ │ ├── 02_create_search.down.sql
│ │ │ ├── 02_create_search.up.sql
│ │ │ ├── 03_misc.up.sql
│ │ │ ├── 04_image_tables.up.sql
│ │ │ ├── 05_edits.up.sql
│ │ │ ├── 06_deletion_and_redirects.up.sql
│ │ │ ├── 07_optimization_indexes.up.sql
│ │ │ ├── 08_user_invite.up.sql
│ │ │ ├── 09_image_data.up.sql
│ │ │ ├── 10_tag_categories.up.sql
│ │ │ ├── 11_image_constraints.up.sql
│ │ │ ├── 12_fix_performer_trigger.up.sql
│ │ │ ├── 13_sort_indexes.up.sql
│ │ │ ├── 14_phash_distance_search.up.sql
│ │ │ ├── 15_scene_fingerprint_submissions.up.sql
│ │ │ ├── 16_fix_scene_update_trigger.up.sql
│ │ │ ├── 17_edit_votes.up.sql
│ │ │ ├── 18_fingerprint_user.up.sql
│ │ │ ├── 19_scene_created_index.up.sql
│ │ │ ├── 20_edit_constraints.up.sql
│ │ │ ├── 21_site_urls.up.sql
│ │ │ ├── 22_performer_search_indexes.up.sql
│ │ │ ├── 23_favorites.up.sql
│ │ │ ├── 24_drafts.up.sql
│ │ │ ├── 25_scene_codes.up.sql
│ │ │ ├── 26_scene_partial_date.down.sql
│ │ │ ├── 26_scene_partial_date.up.sql
│ │ │ ├── 27_edit_closed_at.up.sql
│ │ │ ├── 28_studio_favorite_index.up.sql
│ │ │ ├── 29_scene_edit_fingerprint_index.up.sql
│ │ │ ├── 30_edit_bot.up.sql
│ │ │ ├── 31_scenes_deleted_idx.up.sql
│ │ │ ├── 32_edit_indexes.up.sql
│ │ │ ├── 33_invite_key_uses.up.sql
│ │ │ ├── 34_fingerprints.up.sql
│ │ │ ├── 35_websearch.up.sql
│ │ │ ├── 36_drop_unique_invite.up.sql
│ │ │ ├── 37_tokens.up.sql
│ │ │ ├── 38_scenes_studio_id_index.up.sql
│ │ │ ├── 39_edits_updates.up.sql
│ │ │ ├── 40_fingerprint_vote.up.sql
│ │ │ ├── 41_notifications.up.sql
│ │ │ ├── 42_date_columns.up.sql
│ │ │ ├── 43_studio_aliases.up.sql
│ │ │ ├── 44_performer_death_date.up.sql
│ │ │ ├── 45_scene_production_date.up.sql
│ │ │ ├── 46_update_default_notifications.up.sql
│ │ │ ├── 47_favorite_unique.up.sql
│ │ │ ├── 48_fingerprinted_scene_edit_notification.up.sql
│ │ │ ├── 49_entity_search_lower_idx.up.sql
│ │ │ ├── 50_rename_url_siteid.up.sql
│ │ │ ├── 51_scene_deleted_sort_indexes.up.sql
│ │ │ ├── 52_fingerprint_hash_bigint.up.sql
│ │ │ ├── 53_varchar_to_text.up.sql
│ │ │ ├── 54_delete_soft_deleted_aliases.up.sql
│ │ │ ├── 55_fix_edit_data_dates.up.sql
│ │ │ ├── 56_paradedb_search.up.sql
│ │ │ └── 57_mod_audit.up.sql
│ │ └── testutil/
│ │ └── testutil.go
│ ├── dataloader/
│ │ ├── bodymodificationsloader_gen.go
│ │ ├── boolsloader_gen.go
│ │ ├── editcommentloader_gen.go
│ │ ├── editloader_gen.go
│ │ ├── fingerprintsloader_gen.go
│ │ ├── imageloader_gen.go
│ │ ├── loaders.go
│ │ ├── performerloader_gen.go
│ │ ├── sceneappearancesloader_gen.go
│ │ ├── sceneloader_gen.go
│ │ ├── siteloader_gen.go
│ │ ├── stringsloader_gen.go
│ │ ├── studioloader_gen.go
│ │ ├── submittedfingerprintsloader_gen.go
│ │ ├── tagcategoryloader_gen.go
│ │ ├── tagloader_gen.go
│ │ ├── urlloader_gen.go
│ │ └── uuidsloader_gen.go
│ ├── email/
│ │ ├── manager.go
│ │ ├── templates/
│ │ │ ├── email.html
│ │ │ └── email.txt
│ │ └── user.go
│ ├── image/
│ │ ├── cache/
│ │ │ └── cache.go
│ │ ├── resize_unix.go
│ │ ├── resize_windows.go
│ │ └── sort.go
│ ├── models/
│ │ ├── assign/
│ │ │ └── assign.go
│ │ ├── extension_criterion_input.go
│ │ ├── extension_edit_details.go
│ │ ├── extension_edit_details_test.go
│ │ ├── extension_role_enum.go
│ │ ├── generate.go
│ │ ├── generated_exec.go
│ │ ├── generated_models.go
│ │ ├── model_draft.go
│ │ ├── model_edit.go
│ │ ├── model_image.go
│ │ ├── model_invite_key.go
│ │ ├── model_mod_audit.go
│ │ ├── model_notification.go
│ │ ├── model_performer.go
│ │ ├── model_scene.go
│ │ ├── model_site.go
│ │ ├── model_studio.go
│ │ ├── model_tag.go
│ │ ├── model_tag_category.go
│ │ ├── model_user.go
│ │ ├── model_user_tokens.go
│ │ ├── scalars.go
│ │ ├── translate.go
│ │ ├── url.go
│ │ └── validator/
│ │ └── validator.go
│ ├── queries/
│ │ ├── copyfrom.go
│ │ ├── db.go
│ │ ├── draft.sql.go
│ │ ├── edit.sql.go
│ │ ├── fingerprint.sql.go
│ │ ├── helpers.go
│ │ ├── image.sql.go
│ │ ├── invite_key.sql.go
│ │ ├── mod_audit.sql.go
│ │ ├── models.go
│ │ ├── notification.sql.go
│ │ ├── performer.sql.go
│ │ ├── querier.go
│ │ ├── scene.sql.go
│ │ ├── site.sql.go
│ │ ├── sql/
│ │ │ ├── draft.sql
│ │ │ ├── edit.sql
│ │ │ ├── fingerprint.sql
│ │ │ ├── image.sql
│ │ │ ├── invite_key.sql
│ │ │ ├── mod_audit.sql
│ │ │ ├── notification.sql
│ │ │ ├── performer.sql
│ │ │ ├── scene.sql
│ │ │ ├── site.sql
│ │ │ ├── studio.sql
│ │ │ ├── tag.sql
│ │ │ ├── tag_category.sql
│ │ │ ├── user.sql
│ │ │ └── user_token.sql
│ │ ├── studio.sql.go
│ │ ├── tag.sql.go
│ │ ├── tag_category.sql.go
│ │ ├── types.go
│ │ ├── user.sql.go
│ │ └── user_token.sql.go
│ ├── service/
│ │ ├── draft/
│ │ │ └── service.go
│ │ ├── edit/
│ │ │ ├── edit.go
│ │ │ ├── modbot.go
│ │ │ ├── performer.go
│ │ │ ├── query.go
│ │ │ ├── scene.go
│ │ │ ├── service.go
│ │ │ ├── studio.go
│ │ │ ├── tag.go
│ │ │ └── validate.go
│ │ ├── errutil/
│ │ │ └── errors.go
│ │ ├── factory.go
│ │ ├── image/
│ │ │ ├── service.go
│ │ │ └── utils.go
│ │ ├── interface.go
│ │ ├── invite/
│ │ │ └── service.go
│ │ ├── mod_audit/
│ │ │ └── mod_audit.go
│ │ ├── notification/
│ │ │ └── service.go
│ │ ├── performer/
│ │ │ ├── joins.go
│ │ │ ├── query.go
│ │ │ └── service.go
│ │ ├── query/
│ │ │ ├── criterion.go
│ │ │ └── helpers.go
│ │ ├── scene/
│ │ │ ├── query.go
│ │ │ └── service.go
│ │ ├── site/
│ │ │ ├── query.go
│ │ │ └── service.go
│ │ ├── studio/
│ │ │ ├── query.go
│ │ │ └── service.go
│ │ ├── tag/
│ │ │ ├── query.go
│ │ │ └── service.go
│ │ ├── user/
│ │ │ ├── activation.go
│ │ │ ├── apikey.go
│ │ │ ├── invite.go
│ │ │ ├── joins.go
│ │ │ ├── password.go
│ │ │ ├── query.go
│ │ │ ├── service.go
│ │ │ ├── token.go
│ │ │ ├── user.go
│ │ │ └── validate.go
│ │ └── usertoken/
│ │ └── service.go
│ └── storage/
│ ├── favicon.go
│ ├── file.go
│ ├── image_backend.go
│ └── s3.go
├── pkg/
│ ├── logger/
│ │ ├── logger.go
│ │ ├── otel.go
│ │ └── progress_formatter.go
│ └── utils/
│ ├── arguments.go
│ ├── crypto.go
│ ├── date.go
│ ├── enum.go
│ ├── file.go
│ ├── json.go
│ ├── password_blacklist.go
│ ├── slice_compare.go
│ └── slice_compare_test.go
├── scripts/
│ └── getDate.go
└── sqlc.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
####
# Visual Studio Code
####
.vscode
####
# Jetbrains
####
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Vim
*.swp
####
# Random
####
frontend/node_modules/*
frontend/build/*
*.db
stashdb
stash-box
dist
pkg/models/generated_*.go
# TODO - we'll add this in later
vendor
docker
================================================
FILE: .gitattributes
================================================
go.mod text eol=lf
go.sum text eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: "[Bug Report] Short Form Subject (50 Chars or less)"
labels: help wanted
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem please ensure that your screenshots are SFW or at least appropriately censored.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/discussion---request-for-commentary--rfc-.md
================================================
---
name: Discussion / Request for Commentary [RFC]
about: This is for issues that will be discussed and won't necessarily result directly
in commits or pull requests.
title: "[RFC] Short Form Title"
labels: help wanted
assignees: ''
---
<!-- Update or delete the title if you need to delegate your title gore to something
# Title
*### Scope*
<!-- describe the scope of your topic and your goals ideally within a single paragraph or TL;DR kind of summary so its easier for people to determine if they can contribute at a glance. -->
## Long Form
<!-- Only required if your scope and titles can't cover everything. -->
## Examples
<!-- if you can show a picture or video examples post them here, please ensure that you respect people's time and attention and understand that people are volunteering their time, so concision is ideal and considerate. -->
## Reference Reading
<!-- if there is any reference reading or documentation, please refer to it here. -->
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] Short Form Title (50 chars or less.)"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
release:
types: [ published ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build custom PostgreSQL image
run: docker build -t stash-box-postgres -f docker/production/postgres/Dockerfile docker/production/postgres
- name: Start PostgreSQL container
run: |
docker run -d \
--name postgres \
-e POSTGRES_DB=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_USER=postgres \
-p 5432:5432 \
--health-cmd pg_isready \
--health-interval 10s \
--health-timeout 5s \
--health-retries 5 \
stash-box-postgres
- name: Wait for PostgreSQL to be ready
run: |
until docker exec postgres pg_isready; do
echo "Waiting for postgres..."
sleep 2
done
- name: Install vips
run: sudo apt-get update && sudo apt-get install -y libvips-dev
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.25.x
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install PNPM
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install sqlc
uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: '1.29.0'
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node_modules
with:
path: frontend/node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('frontend/pnpm-lock.yaml') }}
- name: Cache UI build
uses: actions/cache@v4
id: cache-ui
env:
cache-name: cache-ui
with:
path: frontend/build
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('frontend/pnpm-lock.yaml', 'frontend/vite.config.js', 'frontend/src/**', 'graphql/**/*.graphql') }}
- name: Cache go build
uses: actions/cache@v4
env:
cache-name: cache-go-cache-2
with:
path: |
~/go/pkg/mod
.go-cache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('go.mod', '**/go.sum') }}
- name: Pre-install
run: make pre-ui
- name: Generate
run: make generate
- name: Check generated files
run: |
if ! git diff --exit-code; then
echo "::error::Generated files have changed. Run 'make generate' locally and commit the changes."
git diff
exit 1
fi
- name: Build UI
# skip UI build for pull requests if UI is unchanged (UI was cached)
# this means that the build version/time may be incorrect if the UI is
# not changed in a pull request
if: ${{ github.event_name != 'pull_request' || steps.cache-ui.outputs.cache-hit != 'true' }}
run: make ui
- name: Run tests
env:
POSTGRES_DB: postgres:postgres@localhost/postgres?sslmode=disable
run: make it
- name: Set PR version
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/master'}}
run: echo "BUILD_TYPE=PR" >> $GITHUB_ENV
- name: Set Official version
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest-develop' }}
run: echo "BUILD_TYPE=OFFICIAL" >> $GITHUB_ENV
- name: Set Development version
if: ${{ github.event_name == 'push' }}
run: echo "BUILD_TYPE=DEVELOPMENT" >> $GITHUB_ENV
- name: Crosscompile binaries
run: make cross-compile
env:
BUILD_TYPE: "${{ env.BUILD_TYPE }}"
- name: Generate checksums
run: |
git describe --tags --exclude latest-develop | tee CHECKSUMS_SHA1
sha1sum dist/stash-box-* | sed 's/dist\/.*\///g' | tee -a CHECKSUMS_SHA1
echo "STASH_BOX_VERSION=$(git describe --tags --exclude latest-develop)" >> $GITHUB_ENV
- name: Upload Windows binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v4
with:
name: stash-box-win.exe
path: dist/stash-box-windows.exe
- name: Upload Linux binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v4
with:
name: stash-box-linux
path: dist/stash-box-linux
- name: Update latest-develop tag
if: ${{ github.event_name == 'push' }}
run : git tag -f latest-develop; git push -f --tags
- name: Development Release
if: ${{ github.event_name == 'push' }}
uses: marvinpinto/action-automatic-releases@v1.1.2
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: true
automatic_release_tag: latest-develop
title: "${{ env.STASH_BOX_VERSION }}: Latest development build"
files: |
dist/stash-box-windows.exe
dist/stash-box-linux
CHECKSUMS_SHA1
- name: Master release
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest-develop' }}
uses: WithoutPants/github-release@v2.0.4
with:
token: "${{ secrets.GITHUB_TOKEN }}"
allow_override: true
files: |
dist/stash-box-windows.exe
dist/stash-box-linux
CHECKSUMS_SHA1
gzip: false
- name: Login to DockerHub
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Development Docker
if: ${{ github.event_name == 'push' }}
run: |
docker build -t stashapp/stash-box:development -f ./docker/ci/x86_64/Dockerfile ./dist
docker push stashapp/stash-box:development
- name: Release Docker
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest-develop' }}
run: |
docker build -t stashapp/stash-box:latest -f ./docker/ci/x86_64/Dockerfile ./dist
docker push stashapp/stash-box:latest
================================================
FILE: .github/workflows/frontend-lint.yml
================================================
name: Lint (frontend)
on:
push:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install PNPM
uses: pnpm/action-setup@v4
with:
version: 9
- name: Cache node packages
uses: actions/cache@v4
env:
cache-name: cache-node_modules
with:
path: frontend/node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('frontend/pnpm-lock.yaml') }}
- name: Install node packages
run: make pre-ui
- name: Validate UI
run: make ui-validate
================================================
FILE: .github/workflows/golangci-lint.yml
================================================
name: Lint (golangci-lint)
on:
push:
pull_request:
jobs:
golangci:
name: lint
runs-on: ubuntu-24.04
steps:
- name: Install vips
run: sudo apt-get update && sudo apt-get install -y libvips-dev
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.25.x
- run: mkdir frontend/build && touch frontend/build/dummy
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
####
# Visual Studio Code
####
.vscode
####
# Jetbrains
####
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Vim
*.swp
# macOS
.DS_Store
####
# Random
####
node_modules
*.db
.go-cache/
stashdb
/stash-box
dist
stash-box-config.yml
# TODO - we'll add this in later
vendor
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
go: "1.25"
linters:
enable:
- copyloopvar
- dogsled
- errorlint
- exhaustive
- gocritic
- misspell
- noctx
- revive
- rowserrcheck
- sqlclosecheck
settings:
exhaustive:
default-signifies-exhaustive: true
revive:
confidence: 0.8
severity: error
rules:
- name: blank-imports
disabled: true
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: if-return
disabled: false
- name: increment-decrement
- name: var-naming
arguments:
- - IDS
- []
- - skip-package-name-checks: true
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
disabled: true
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Stash-box is an open-source video indexing and metadata API server for adult content, developed by Stash App. It serves as a community-driven database of metadata, similar to MusicBrainz for music. The application uses Go for the backend API with GraphQL, and React/TypeScript for the frontend.
## Development Commands
### Backend Development
- `make build` - Build the Go binary with embedded frontend
- `make test` - Run unit tests
- `make it` - Run integration tests
- `make lint` - Run golangci-lint on Go code
- `make fmt` - Format Go code with gofmt
- `make generate` - Regenerate GraphQL files, sqlc queries, and UI types
- `make generate-backend` - Generate Go GraphQL files only
- `make generate-ui` - Generate frontend GraphQL types only
- `make generate-sqlc` - Generate sqlc query code only
- `make generate-goverter` - Generate goverter type conversion code
- `make generate-dataloaders` - Generate dataloader files
### Frontend Development
- `make pre-ui` - Install frontend dependencies via pnpm
- `make ui` - Build the frontend for production
- `make ui-start` - Start frontend development server
- `make ui-fmt` - Format frontend code with prettier
- `make ui-validate` - Run linting, format checking, and TypeScript validation
### Complete Build Process
- `make stash-box` - Full build: dependencies, generation, UI, linting, and binary
### Database Setup
The application requires PostgreSQL with specific extensions:
- Run `CREATE EXTENSION pg_trgm; CREATE EXTENSION pgcrypto;` as superuser before first run
- Database schema migrations run automatically on startup
- **Migrations**: Located in `internal/database/migrations/postgres/` and executed sequentially by filename
- Default connection string: `postgres@localhost/stash-box?sslmode=disable`
- **Query Code Generation**: Uses sqlc (configured in `sqlc.yaml`) to generate type-safe Go code from SQL queries
## Architecture
### Backend (`internal/` directory)
- **`internal/api/`** - GraphQL resolvers, HTTP handlers, and server setup
- **`internal/auth/`** - Authentication and authorization logic
- **`internal/models/`** - Data models, database entity definitions, and generated GraphQL types
- **`internal/database/`** - Database connection, migrations, and PostgreSQL-specific code
- **`internal/queries/`** - Type-safe database query code generated by sqlc from SQL files
- **`internal/service/`** - Business logic layer organized by entity (draft, edit, image, performer, scene, site, studio, tag, user, etc.)
- **`internal/email/`** - Email handling and templates
- **`internal/image/`** - Image processing, storage (local/S3), caching, and resizing
- **`internal/storage/`** - Storage abstraction layer for local/S3 backends
- **`internal/config/`** - Configuration management and parsing
- **`internal/converter/`** - Generated type conversion code via goverter
- **`internal/dataloader/`** - Generated DataLoader implementations for efficient GraphQL N+1 query resolution
- **`internal/cron/`** - Scheduled task management
### Frontend (`frontend/` directory)
- **`frontend/src/pages/`** - Page components organized by entity type (performers, scenes, studios, tags, users)
- **`frontend/src/components/`** - Reusable UI components and form elements
- **`frontend/src/graphql/`** - GraphQL queries, mutations, fragments, and generated TypeScript types
- **`frontend/src/hooks/`** - Custom React hooks for authentication, pagination, and state management
- **`frontend/src/utils/`** - Utility functions for data transformation, routing, and validation
### Key Concepts
- **Entities**: Performers, Scenes, Studios, Tags, Sites - the main data types in the system
- **Edits**: All changes go through an edit/voting system before being applied to entities
- **Drafts**: Temporary submissions that can be converted to edits
- **Roles**: User permission system (READ, VOTE, EDIT, MODIFY, ADMIN) controlling access to operations
- **Images**: Stored locally or on S3, with automatic resizing and caching capabilities
- **Fingerprints**: Used for scene matching and duplicate detection via three algorithms:
- **MD5**: Hash of entire video file for exact matching
- **OSHASH**: OpenSubtitles Hash implementation - hash of leading and trailing 64kb of video file
- **PHASH**: Perceptual hash based on a 5x5 grid of frames at regular intervals for content-based matching
### GraphQL Schema
- Schema files in `graphql/schema/` define the API structure
- Code generation via gqlgen creates Go resolvers and TypeScript types
- Uses dataloader pattern to prevent N+1 queries when fetching related data
- **Authorization Directives**: `@hasRole(role: ROLE)` implemented in `internal/api/directives.go` for field-level access control
### Configuration
- YAML configuration file (`stash-box-config.yml`) controls server behavior
- Support for PostgreSQL connection settings, image storage, email, and security options
- Environment variables can override configuration values
## Testing
### Backend Tests
- **Integration tests preferred**: All tests should be integration tests utilizing the GraphQL API as much as possible
- Unit tests: `make test` - Fast tests that don't require database (use sparingly)
- Integration tests: `make it` - Full tests against PostgreSQL test database
- Integration tests use PostgreSQL with default connection string `postgres@localhost/stash-box-test?sslmode=disable`
- Can override test database via `POSTGRES_DB` environment variable
- **Warning**: Integration tests drop all tables, never run against production database
- Test files follow pattern `internal/api/*_integration_test.go` with GraphQL client helper in `graphql_client_test.go`
- **Test Data Setup**: Tests use the service layer to create test data (see `internal/api/integration_test.go` for user setup example)
### Frontend Tests
- `cd frontend && pnpm run validate` - Runs ESLint, stylelint, prettier check, and TypeScript compilation
- No unit tests currently implemented in frontend
## Development Workflow
1. **Setup**: Ensure PostgreSQL is running with required extensions
2. **Dependencies**: Run `make pre-ui` to install frontend packages
3. **Development**: Use `make ui-start` for frontend development server
4. **API Development**: Modify GraphQL schema, run `make generate` to update code
5. **Database Changes**: Add migration files to `internal/database/migrations/postgres/` (executed sequentially by filename)
6. **Query Changes**: Modify SQL files in `internal/queries/sql/`, run `make generate-sqlc` to regenerate Go code
7. **Testing**: Run `make lint test it` before committing changes
8. **Build**: Use `make stash-box` for complete production build
## Important Notes
- The application requires libvips for image processing on Linux systems
- Default admin user (`root`) is created on first run with random password printed to stdout
- Frontend development can use API key in `.env.development.local` to bypass login
- Integration tests require PostgreSQL (default: `postgres@localhost/stash-box-test?sslmode=disable`)
- pHash distance matching requires `pg-spgist_hamming` PostgreSQL extension
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 stashapp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
LDFLAGS := $(LDFLAGS)
.PHONY: \
stash-box \
generate \
generate-backend \
generate-ui \
generate-sqlc \
generate-goverter \
generate-dataloaders \
test \
it \
fmt \
lint \
ui \
ui-start \
ui-fmt \
ui-validate \
pre-ui \
clean
ifdef OUTPUT
OUTPUT := -o $(OUTPUT)
endif
stash-box: pre-ui generate ui lint build
pre-build:
ifndef BUILD_DATE
$(eval BUILD_DATE := $(shell go run scripts/getDate.go))
endif
ifndef GITHASH
$(eval GITHASH := $(shell git rev-parse --short HEAD))
endif
ifndef STASH_BOX_VERSION
$(eval STASH_BOX_VERSION := $(shell git describe --tags --abbrev=0 --exclude latest-develop))
endif
ifndef BUILD_TYPE
$(eval BUILD_TYPE := LOCAL)
endif
build: pre-build
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash-box/internal/api.version=$(STASH_BOX_VERSION)' -X 'github.com/stashapp/stash-box/internal/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash-box/internal/api.githash=$(GITHASH)' -X 'github.com/stashapp/stash-box/internal/api.buildtype=$(BUILD_TYPE)')
go build $(OUTPUT) -v -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS)" ./cmd/stash-box
build-release-static: EXTRA_LDFLAGS := -extldflags=-static -s -w
build-release-static: build
# Regenerates GraphQL files and sqlc code
generate: generate-backend generate-ui generate-sqlc
clean:
@ rm -rf stash-box frontend/node_modules frontend/build dist
generate-backend:
@ go generate ./...
generate-ui:
cd frontend && pnpm generate
generate-sqlc:
sqlc generate
generate-goverter:
go run github.com/jmattheis/goverter/cmd/goverter gen ./internal/converter/gen
generate-dataloaders:
cd internal/dataloader; \
go run github.com/vektah/dataloaden UUIDsLoader github.com/gofrs/uuid.UUID "[]github.com/gofrs/uuid.UUID"; \
go run github.com/vektah/dataloaden URLLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.URL"; \
go run github.com/vektah/dataloaden TagLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Tag"; \
go run github.com/vektah/dataloaden StringsLoader github.com/gofrs/uuid.UUID "[]string"; \
go run github.com/vektah/dataloaden SceneAppearancesLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.PerformerScene"; \
go run github.com/vektah/dataloaden PerformerLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Performer"; \
go run github.com/vektah/dataloaden ImageLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Image"; \
go run github.com/vektah/dataloaden FingerprintsLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.Fingerprint"; \
go run github.com/vektah/dataloaden SubmittedFingerprintsLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.Fingerprint"; \
go run github.com/vektah/dataloaden BodyModificationsLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.BodyModification"; \
go run github.com/vektah/dataloaden TagCategoryLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.TagCategory"; \
go run github.com/vektah/dataloaden SiteLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Site"; \
go run github.com/vektah/dataloaden StudioLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Studio"; \
go run github.com/vektah/dataloaden EditLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Edit"; \
go run github.com/vektah/dataloaden EditCommentLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.EditComment"; \
go run github.com/vektah/dataloaden SceneLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Scene"; \
go run github.com/vektah/dataloaden BoolsLoader github.com/gofrs/uuid.UUID "bool";
test:
go test ./...
# Runs the integration tests. -count=1 is used to ensure results are not
# cached, which is important if the environment changes
it:
go test -tags=integration -count=1 ./...
# Runs gofmt -w on the project's source code, modifying any files that do not match its style.
fmt:
go fmt ./...
# Runs all configured linuters. golangci-lint needs to be installed locally first.
lint:
golangci-lint run
pre-ui:
cd frontend && pnpm install
ui:
cd frontend && pnpm build
ui-start:
cd frontend && pnpm start
ui-fmt:
cd frontend && pnpm format
# runs tests and checks on the UI and builds it
ui-validate:
cd frontend && pnpm run validate
# cross-compile- targets should be run within the compiler docker container
cross-compile-windows: export GOOS := windows
cross-compile-windows: export GOARCH := amd64
cross-compile-windows: export CC := x86_64-w64-mingw32-gcc
cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
cross-compile-windows: export CGO_ENABLED = 0
cross-compile-windows: OUTPUT := -o dist/stash-box-windows.exe
cross-compile-windows: build-release-static
cross-compile-linux: export GOOS := linux
cross-compile-linux: export GOARCH := amd64
cross-compile-linux: OUTPUT := -o dist/stash-box-linux
cross-compile-linux: export CGO_ENABLED = 1
cross-compile-linux: build
cross-compile:
make cross-compile-windows
make cross-compile-linux
================================================
FILE: README.md
================================================
# stash-box
[](https://github.com/stashapp/stash-box/actions/workflows/build.yml)
[](https://opencollective.com/stashapp)
[](https://goreportcard.com/report/github.com/stashapp/stash-box)
[](https://discord.gg/2TsNFKt)
[](https://github.com/stashapp/stash-box/releases/latest)
[](https://github.com/stashapp/stash-box/labels/bounty)
Stash-box is an open-source video indexing and metadata API server for porn developed by Stash App. The purpose of stash-box is to provide a community-driven database of porn metadata, similar to what MusicBrainz does for music. The submission and editing of metadata should follow the same principles as MusicBrainz. [Learn more here](https://musicbrainz.org/doc/Editing_FAQ). Installing Stash-box will create an empty database for you to populate.
# Canonical community-database
If you're a Stash user, you don't need to install stash-box. The Stash community has a server with many titles from which you can pull data. You can get the login information from our guide to [Accessing StashDB](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stashdb/).
# Docker install
You can find a `docker-compose` file for production deployment [here](docker/production/docker-compose.yml). You can omit Traefik if you don't need a reverse proxy.
If you already have PostgreSQL installed, you can install stash-box on its own from [Docker Hub](https://hub.docker.com/r/stashapp/stash-box).
# Bare-metal install
Stash-box supports macOS, Windows, and Linux. Releases for Windows and Linux can be found [here](https://github.com/stashapp/stash-box/releases).
## Prerequisites
To build stash-box on linux [libvips](https://www.libvips.org/) must be installed, as well as gcc.
## Initial setup
1. Run `make` to build the application.
2. Stash-box requires access to a PostgreSQL database server. Suppose stash-box doesn't find a configuration file (defaults to `stash-box-config.yml` in the current directory). In that case, it will generate a default configuration file with a default PostgreSQL connection string (`postgres@localhost/stash-box?sslmode=disable`). You can adjust the connection string as needed.
3. The database must be created and available. If the PostgreSQL user is not a superuser, run `CREATE EXTENSION pg_trgm; CREATE EXTENSION pgcrypto;` by a superuser before rerunning Stash-box. If the schema is not present, it will be created within the database.
4. The `sslmode` parameter is documented [here](https://godoc.org/github.com/lib/pq). Use `sslmode=disable` to not use SSL for the database connection. The default is `require`.
5. After ensuring the database connection and availability, rerun Stash-box.
#### Schema migrations and initial Admin user
The second time that stash-box is run, stash-box will run the schema migrations to create the required tables. It will also generate a `root` user with a random password and an API key. These credentials are printed once to stdout and are not logged. The system will regenerate the root user on startup if it does not exist. You can force the system to create a new root user by deleting the root user row from the database and restarting Stash-box. You'll need to capture the console output with your Admin user on the first successful StashDB executable start. Otherwise, you will need to allow Postgres to re-create the database before it will re-post a new `root` user.
# Stash-box CLI and configuration
Stash-box is a tool with command line options to make it easier. To see what options are available, run `stash-box --help` in your terminal.
Here's an example of how you can run stash-box locally on port 80:
`stash-box --host 127.0.0.1 --port 80`
**Note:** This command should work on OSX / Linux.
When you start stash-box for the first time, it generates a configuration file called `stash-box-config.yml` in your current working directory. This file contains default settings for stash-box, including:
- Host: `0.0.0.0`
- Port: `9998`
You can change these defaults if needed. For example, if you want to disable the GraphQL playground and cross-domain cookies, you can set `is_production` to `true`.
## API keys and authorization
There are two ways to authenticate a user in Stash-box: a session or an API key.
1. Session-based authentication: To log in, send a request to `/login` with the `username` and `password` in plain text as form values. Session-based authentication will set a cookie that is required for all subsequent requests. To log out, send a request to `/logout`.
2. API key authentication: To use an API key, set the `ApiKey` header to the user's API key value.
### Configuration keys
| Key | Default | Description |
|-----|---------|-------------|
| `title` | `Stash-Box` | Title of the instance, used in the page title. |
| `require_invite` | `true` | If true, users are required to enter an invite key, generated by existing users to create a new account. |
| `require_activation` | `false` | If true, users are required to verify their email address before creating an account. Requires `email_from`, `email_host`, and `host_url` to be set. |
| `activation_expiry` | `7200` (2 hours) | The time - in seconds - after which an activation key (emailed to the user for email verification or password reset purposes) expires. |
| `email_cooldown` | `300` (5 minutes) | The time - in seconds - that a user must wait before submitting an activation or reset password request for a specific email address. |
| `default_user_roles` | `READ`, `VOTE`, `EDIT` | The roles assigned to new users when registering. This field must be expressed as a yaml array. |
| `guidelines_url` | (none) | URL to link to a set of guidelines for users contributing edits. Should be in the form of `https://hostname.com`. |
| `vote_promotion_threshold` | (none) | Number of approved edits before a user automatically has the `VOTE` role assigned. Leave empty to disable. |
| `vote_application_threshold` | `3` | Number of same votes required for immediate application of an edit. Set to zero to disable automatic application. |
| `voting_period` | `345600` | Time, in seconds, before a voting period is closed. |
| `min_destructive_voting_period` | `172800` | Minimum time, in seconds, that needs to pass before a destructive edit can be immediately applied with sufficient positive votes. |
| `vote_cron_interval` | `5m` | Time between runs to close edits whose voting periods have ended. |
| `edit_update_limit` | `1` | Number of times an edit can be updated by the creator. |
| `email_host` | (none) | Address of the SMTP server. Required to send emails for activation and recovery purposes. |
| `email_port` | `25` | Port of the SMTP server. Only STARTTLS is supported. Direct TLS connections are not supported. |
| `email_user` | (none) | Username for the SMTP server. Optional. |
| `email_password` | (none) | Password for the SMTP server. Optional. |
| `email_from` | (none) | Email address from which to send emails. |
| `host_url` | (none) | Base URL for the server. Used when sending emails. Should be in the form of `https://hostname.com`. |
| `image_location` | (none) | Path to store images, for local image storage. An error will be displayed if this is not set when creating non-URL images. |
| `image_backend` | (`file`) | Storage solution for images. Can be set to either `file` or `s3`. |
| `image_jpeg_quality` | `75` | Quality setting when resizing JPEG images. Valid values are 0-100. |
| `image_max_size` | (none) | Max size of image, if no size is specified. Omit to return full size. |
| `image_resizing.enabled` | false | Whether to resize images shown in the frontend. |
| `image_resizing.cache_path` | (none) | Folder in which resized images will be saved for later requests. Recommended when resizing is enabled. |
| `image_resizing.min_size` | (none) | Only resize images above a certain size |
| `userLogFile` | (none) | Path to the user log file, which logs user operations. If not set, then these will be output to stderr. |
| `s3.endpoint` | (none) | Hostname to s3 endpoint used for image storage. |
| `s3.bucket` | (none) | Name of S3 bucket used to store images. |
| `s3.access_key` | (none) | Access key used for authentication. |
| `s3.secret ` | (none) | Secret Access key used for authentication. |
| `s3.max_dimension` | (none) | If set, a resized copy will be created for any image whose dimensions exceed this number. This copy will be served in place of the original. |
| `s3.upload_headers` | (none) | A map of headers to send with each upload request. For example, DigitalOcean requires the `x-amz-acl` header to be set to `public-read` or it does not make the uploaded images available. |
| `phash_distance` | 0 | Determines what binary distance is considered a match when querying with a pHash fingeprint. Using more than 8 is not recommended and may lead to large amounts of false positives. **Note**: The [pg-spgist_hamming extension](#phash-distance-matching) must be installed to use distance matching, otherwise you will get errors. |
| `favicon_path` | (none) | Location where favicons for linked sites should be stored. Leave empty to disable. |
| `draft_time_limit` | (24h) | Time, in seconds, before a draft is deleted. |
| `profiler_port` | 0 | Port on which to serve pprof output. Omit to disable entirely. |
| `port` | 9998 | Port on which the server runs. When using SSL certificates it should be set to `443`. |
| `postgres.max_open_conns` | (0) | Maximum number of concurrent open connections to the database. |
| `postgres.max_idle_conns` | (0) | Maximum number of concurrent idle database connections. |
| `postgres.conn_max_lifetime` | (0) | Maximum lifetime in minutes before a connection is released. |
| `require_scene_draft` | false | Whether to allow scene creation outside of draft submissions. |
| `require_tag_role` | false | Whether to require the EditTag role to edit tags. |
| `csp` | (none) | Contents of the `Content-Security-Policy` header |
| `autocert.enabled` | (none) | Whether to enable [autocert](#lets-encrypt)|
| `autocert.cache_dir` | (none) | The directory where autocert certificates are stored. Should be a persisted directory to avoid certificate regeneration on server restart. |
| `autocert.domain` | (none) | The domain to generate certificates for.|
| `autocert.email` | (none) | A valid email. Will be submitted to Let's Encrypt, but otherwise not made public. |
| `mod_audit_retention_days` | 30 | Number of days to retain audit logs of moderator actions. Set `0` to disable. |
## SSL (HTTPS)
### Let's Encrypt
Stash-box supports automatic certificate generation from Let's Encrypt. If you want to avoid running behind a reverse proxy - which can cause a certain amount of overhead due to image loading - this is the recommended approach.
To use Let's Encrypt, configure the `autocert` config option. As an example:
```
autocert:
enabled: true
cache_dir: /autocert
domain: example.org
email: email@example.org
```
To use autocert it needs access to port 80 to respond to Let's Encrypt's challenge. After certificate generation is done, port 80 will redirect to the SSL port.
Stash-box will automatically renew the certificate once it has less than 30 days until expiration.
### Self-signed certificates
Here's an example of how you can do this using OpenSSL:
`openssl req -x509 -newkey rsa:4096 -sha256 -days 7300 -nodes -keyout stash-box.key -out stash-box.crt -extensions san -config <(echo "[req]"; echo distinguished_name=req; echo "[san]"; echo subjectAltName=DNS:stash-box.server,IP:127.0.0.1) -subj /CN=stash-box.server`
You might need to modify the command for your specific setup. You can find more information about creating a self-signed certificate with OpenSSL [here](https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl).
Once you've generated the certificate and key pair, make sure they're named `stash-box.crt` and `stash-box.key` respectively, and place them in the same directory as stash-box. When Stash-box detects these files, it will use HTTPS instead of HTTP.
## pHash Distance Matching
If you want to enable distance matching for pHashes in stash-box, you'll need to install the [pg-spgist_hamming](https://github.com/fake-name/pg-spgist_hamming) Postgres extension.
The recommended way to do this is to use the [docker image](docker/production/postgres/Dockerfile). Still, you can also install it manually by following the build instructions in the pg-spgist_hamming repository.
Suppose you install the extension after you've run the migrations. In that case, you'll need to run migration #14 manually to install the extension and add the index. If you don't want to do this, you can wipe the database, and the migrations will run the next time you start stash-box.
# Join Our Community
We are excited to announce that we have a new home for support, feature requests, and discussions related to Stash and its associated projects. Join our community on the [Discourse forum](https://discourse.stashapp.cc) to connect with other users, share your ideas, and get help from fellow enthusiasts.
# Development
## Install
* [Go](https://golang.org/dl/), minimum version 1.22.
* [golangci-lint](https://golangci-lint.run/) - Linter aggregator
* Follow instructions for your platform from [https://golangci-lint.run/usage/install/](https://golangci-lint.run/usage/install/).
* Run the linters with `make lint`.
* [PNPM](https://pnpm.io/installation) - PNPM package manager
## Commands
* `make generate` - Generate Go GraphQL files. This command should be run if the GraphQL schema has changed.
* `make ui` - Builds the UI.
* `make pre-ui` - Download frontend dependencies
* `make build` - Builds the binary
* `make test` - Runs the unit tests
* `make it` - Runs the unit and integration tests
* `make lint` - Run the linter
* `make fmt` - Formats and aligns whitespace
**Note:** the integration tests run against a temporary sqlite3 database by default. They can be run against a Postgres server by setting the environment variable `POSTGRES_DB` to the Postgres connection string. For example: `postgres@localhost/stash-box-test?sslmode=disable`. **Be aware that the integration tests drop all tables before and after the tests.**
## Frontend development
To run the frontend in development mode, run `pnpm start` from the frontend directory.
When developing, the API key can be set in `frontend/.env.development.local` to avoid having to log in.
When `is_production` is enabled on the server, this is the only way to authorize in the frontend development environment. If the server uses https or runs on a custom port, this also needs to be configured in `.env.development.local`.
See `frontend/.env.development.local.shadow` for examples.
## GraphQL playground
You can access the GraphQL playground at `host:port/playground`, and the GraphQL interface can be found at `host:port/graphql`. To execute queries add a header with your API key: `{"APIKey":"<API_KEY>"}`. The API key can be found on your user page in stash-box.
## Building a release
1. Run `make generate` to create generated files if they have been changed.
2. Run `make ui build` to build the executable for your current platform.
# FAQ
> I have a question that needs to be answered here.
* Join the [Discourse forum](https://discourse.stashapp.cc)
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
* Start a [discussion on GitHub](https://github.com/stashapp/stash-box/discussions)
================================================
FILE: cmd/stash-box/init.go
================================================
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/stashapp/stash-box/internal/config"
"github.com/stashapp/stash-box/pkg/logger"
"github.com/stashapp/stash-box/pkg/utils"
)
func initConfig(configFilePath *string) {
if *configFilePath != "" {
dir, name := parseConfigFilePath(*configFilePath)
viper.SetConfigName(name)
viper.AddConfigPath(dir)
} else {
viper.SetConfigName(config.GetConfigName())
viper.AddConfigPath(".")
}
err := viper.ReadInConfig()
newConfig := false
if err != nil {
newConfig = true
defaultConfigFilePath := config.GetDefaultConfigFilePath()
if *configFilePath != "" {
defaultConfigFilePath = *configFilePath
}
_ = utils.Touch(defaultConfigFilePath)
if err = viper.ReadInConfig(); err != nil {
panic(err)
}
}
if err = config.InitializeDefaults(); err != nil {
panic(err)
}
initEnvs()
if err = viper.ReadInConfig(); err != nil {
panic(err)
}
if err := viper.BindPFlags(pflag.CommandLine); err != nil {
logger.Infof("failed to bind flags: %s", err.Error())
}
if err = config.Initialize(); err != nil {
panic(err)
}
if newConfig {
fmt.Printf(`
A new config file has been generated at %s.
The database connection string has been defaulted to: %s
Please ensure this database is created and available, or change the connection string in the configuration file, then rerun stash-box.`,
viper.GetViper().ConfigFileUsed(), config.GetDatabasePath())
os.Exit(0)
}
missingEmail := config.GetMissingEmailSettings()
if len(missingEmail) > 0 {
fmt.Printf("RequireActivation is set to true, but the following required settings are missing: %s\n", strings.Join(missingEmail, ", "))
}
missingAutocert := config.GetMissingAutocertSettings()
if len(missingAutocert) > 0 {
fmt.Printf("Autocert is enabled, but the following required settings are missing: %s\n", strings.Join(missingAutocert, ", "))
os.Exit(1)
}
}
func parseConfigFilePath(configFilePath string) (string, string) {
dir := filepath.Dir(configFilePath)
name := filepath.Base(configFilePath)
extension := filepath.Ext(configFilePath)
name = strings.TrimSuffix(name, extension)
return dir, name
}
func initEnvs() {
viper.SetEnvPrefix("stash_box")
viper.AutomaticEnv()
_ = viper.BindEnv("host")
_ = viper.BindEnv("port")
_ = viper.BindEnv("database")
}
================================================
FILE: cmd/stash-box/main.go
================================================
package main
import (
"context"
"net"
"github.com/spf13/pflag"
"github.com/stashapp/stash-box/frontend"
"github.com/stashapp/stash-box/internal/api"
"github.com/stashapp/stash-box/internal/config"
"github.com/stashapp/stash-box/internal/cron"
"github.com/stashapp/stash-box/internal/database"
"github.com/stashapp/stash-box/internal/email"
"github.com/stashapp/stash-box/internal/image"
"github.com/stashapp/stash-box/internal/service"
"github.com/stashapp/stash-box/pkg/logger"
)
func main() {
// Initialize flags
pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host")
pflag.Int("port", 9998, "port to serve from")
configFilePath := pflag.String("config_file", "", "location of the config file")
pflag.Parse()
// Initialize config
initConfig(configFilePath)
// Initialize logger
logger.Init(config.GetLogFile(), config.GetUserLogFile(), config.GetLogOut(), config.GetLogLevel())
cleanup := logger.InitTracer()
//nolint:errcheck
defer cleanup(context.Background())
api.InitializeSession()
// Create email manager
emailMgr := email.NewManager()
db := database.Initialize(config.GetDatabasePath())
fac := service.NewFactory(db, emailMgr)
fac.User().CreateSystemUsers(context.Background())
api.Start(*fac, frontend.FS)
cron.Init(*fac)
if err := image.InitResizer(); err != nil {
panic(err)
}
blockForever()
}
func blockForever() {
c := make(chan struct{})
<-c
}
================================================
FILE: docker/build/x86_64/Dockerfile
================================================
# this dockerfile must be built from the top-level stash directory
# ie from top=level stash:
# docker build -t stash-box/build -f docker/build/x86_64/Dockerfile .
FROM golang:1.13.14 as compiler
RUN apt-get update && apt-get install -y apt-transport-https
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && \
apt-get install -y nodejs yarn xz-utils --no-install-recommends || exit 1; \
rm -rf /var/lib/apt/lists/*;
WORKDIR /
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# copy the ui yarn stuff so that it doesn't get rebuilt every time
COPY ./frontend/package.json ./frontend/yarn.lock /stash-box/frontend/
COPY ./Makefile /stash-box/
WORKDIR /stash-box
RUN make pre-ui
COPY . /stash-box/
ENV GO111MODULE=on
RUN make generate
RUN make ui
RUN make build
FROM ubuntu:19.10 as app
RUN apt-get update && apt-get -y install ca-certificates
COPY --from=compiler /stash-box/stash-box /usr/bin/
EXPOSE 9998
CMD ["stash-box", "--config_file", "/root/.stash-box/stash-box-config.yml"]
================================================
FILE: docker/build/x86_64/db/initdb.sh
================================================
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -c 'CREATE DATABASE "stash-box";' && \
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "stash-box" -c 'CREATE EXTENSION pg_trgm; CREATE EXTENSION pgcrypto;'
================================================
FILE: docker/build/x86_64/docker-compose.yml
================================================
# APPNICENAME=Stash-box
# APPDESCRIPTION=Stash App's own OpenSource video indexing and Perceptual Hashing MetaData API for porn
version: '3.4'
services:
db:
image: postgres:12.3
restart: always
environment:
POSTGRES_PASSWORD: stash-box-db
volumes:
- ./stash-box-data/data:/var/lib/postgresql/data
- ./db:/docker-entrypoint-initdb.d
stash-box:
image: stash-box/build:latest
restart: always
depends_on:
- "db"
environment:
STASH_BOX_DATABASE: postgres:stash-box-db@db/stash-box?sslmode=disable
ports:
- 9998:9998
volumes:
- /etc/localtime:/etc/localtime:ro
## Adjust below paths (the left part) to your liking.
## E.g. you can change ./config:/root/.stash to ./stash:/root/.stash
## Keep configs here.
- ./config:/root/.stash-box
================================================
FILE: docker/ci/x86_64/Dockerfile
================================================
# must be built from /dist directory
FROM ubuntu:24.04 as app
LABEL MAINTAINER="https://discord.gg/Uz29ny"
RUN apt-get update && apt-get install -y libvips
COPY stash-box-linux /usr/bin/stash-box
EXPOSE 9998
CMD ["stash-box", "--config_file", "/root/.stash-box/stash-box-config.yml"]
================================================
FILE: docker/ci/x86_64/docker_push.sh
================================================
#!/bin/bash
DOCKER_TAG=$1
# must build the image from dist directory
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker build -t stashapp/stash-box:$DOCKER_TAG -f ./docker/ci/x86_64/Dockerfile ./dist
docker push stashapp/stash-box:$DOCKER_TAG
================================================
FILE: docker/production/docker-compose.yml
================================================
version: '3.8'
services:
postgres:
container_name: postgres
build: ./postgres
restart: always
environment:
POSTGRES_USER: <USER>
POSTGRES_PASSWORD: <PASSWORD>
POSTGRES_DB: <DATABASE>
volumes:
- /pgdata:/var/lib/postgresql/data
stash-box:
container_name: stash-box
image: stashapp/stash-box:development
restart: always
logging:
driver: "json-file"
options:
max-file: "10"
max-size: "2m"
links:
- postgres
volumes:
- <CONFIG_DIR>:/root/.stash-box
labels:
- traefik.http.routers.stash-box.rule=Host(`<DOMAIN>`)
- traefik.http.routers.stash-box.tls=true
- traefik.http.routers.stash-box.tls.certresolver=stash-box
- traefik.port=9998
traefik:
container_name: traefik
image: traefik:2.3
restart: always
ports:
- 80:80
- 443:443
command:
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entryPoints.web.http.redirections.entryPoint.to=websecure"
- "--entryPoints.web.http.redirections.entryPoint.scheme=https"
- "--providers.docker=true"
- "--certificatesResolvers.stash-box.acme.email=<EMAIL>"
- "--certificatesResolvers.stash-box.acme.storage=/acme.json"
- "--certificatesresolvers.stash-box.acme.tlschallenge=true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /traefik/acme.json:/acme.json
================================================
FILE: docker/production/postgres/Dockerfile
================================================
FROM postgres:18
# Build and install pg-spgist_hamming (pHash matching)
RUN apt-get update \
&& apt-get install -y --no-install-recommends make gcc postgresql-server-dev-18 git ca-certificates \
&& git clone --depth 1 https://github.com/fake-name/pg-spgist_hamming.git /tmp/pg-spgist_hamming \
&& make -C /tmp/pg-spgist_hamming/bktree \
&& make -C /tmp/pg-spgist_hamming/bktree install \
&& rm -rf /tmp/pg-spgist_hamming /var/lib/apt/lists/* \
&& apt-get purge -y --auto-remove make gcc postgresql-server-dev-18 git
# Install pg_search (ParadeDB full-text search)
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& curl -fLo /tmp/pg_search.deb \
https://github.com/paradedb/paradedb/releases/download/v0.21.8/postgresql-18-pg-search_0.21.8-1PARADEDB-trixie_amd64.deb \
&& apt-get install -y /tmp/pg_search.deb \
&& rm -f /tmp/pg_search.deb \
&& apt-get purge -y curl \
&& rm -rf /var/lib/apt/lists/*
================================================
FILE: frontend/.gitattributes
================================================
src/**/*.ts* text eol=lf
================================================
FILE: frontend/.gitignore
================================================
.idea/
dist/
node_modules/
src/**/*.jsx
tests/__coverage__/
tests/**/*.jsx
.eslintcache
.env*.local
build
*.swp
================================================
FILE: frontend/.nvmrc
================================================
24
================================================
FILE: frontend/README.md
================================================
# Stash-box frontend
This project builds the frontend for the stash-box server. It can be used to build the static bundle for the go server, or be run standalone for development purposes.
## Setup / Installing
Make sure your environment is up to date:
- node >= `22`
- pnpm >= `9`
For `node` installation instructions, please see the websites for [node.js](https://nodejs.org/en/download/).
`PNPM` can usually be installed by running `corepack enable pnpm`. For other options, see the [PNPM website](https://pnpm.io/installation).
Install dependencies
```shell
pnpm i
```
## GraphQL development
If any queries/mutations or the schema on the server is updated, the Typescript types can be updated with:
```shell
pnpm generate
```
## Running
### Local development server
The API key can be set in the environment configuration. To do so, you will need to initialize the environment configuration:
```shell
cp .env.development.local.shadow .env.development.local
```
Fill in the `VITE_APIKEY` variable in `.env.development.local` with the API key for the user.
Run the local development server:
```shell
pnpm start
```
The server will by default start on [http://localhost:3001](http://localhost:3001) and will automatically be updated whenever any changes are made. The port can be changed by uncommenting the `PORT` entry and setting the value in the `.env.development.local` file.
Run the linter:
```shell
pnpm lint
```
Run the code formatter:
```shell
pnpm format
```
Build the release bundle:
```shell
pnpm build
```
Run the validation (before submitting a Pull Request)
```shell
pnpm validate
```
================================================
FILE: frontend/biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": {
"ignoreUnknown": false,
"includes": ["src/**", "!src/graphql/types.ts"]
},
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
"javascript": { "formatter": { "quoteStyle": "double" }, "globals": [] },
"linter": {
"rules": {
"a11y": {
"useSemanticElements": "off"
},
"correctness": {
"useUniqueElementIds": "off"
}
}
},
"assist": {
"enabled": true,
"actions": { "source": { "organizeImports": "on" } }
}
}
================================================
FILE: frontend/codegen.yml
================================================
overwrite: true
schema: "../graphql/schema/**/*.graphql"
documents: "src/graphql/**/*.gql"
generates:
src/graphql/types.ts:
plugins:
- typescript
- typescript-operations
- typed-document-node
config:
dedupeOperationSuffix: true
scalars:
Date: string
DateTime: string
Time: string
Upload: File
FingerprintHash: string
namingConvention:
enumValues: change-case-all#upperCase
nonOptionalTypename: true
================================================
FILE: frontend/embed.go
================================================
package frontend
import "embed"
//go:embed build
var FS embed.FS
================================================
FILE: frontend/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.json" />
<title>{{.}}</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
================================================
FILE: frontend/package.json
================================================
{
"name": "stash-box-frontend",
"version": "0.1.0",
"description": "Stash-box",
"license": "MIT",
"scripts": {
"start": "vite --host",
"build": "vite build",
"validate": "pnpm lint && pnpm format-check && tsc --noEmit",
"lint": "pnpm biome lint src",
"generate": "graphql-codegen --config codegen.yml",
"format": "pnpm biome format --write",
"format-check": "pnpm biome format",
"analyze": "analyze=true vite build"
},
"engines": {
"node": ">=24",
"yarn": "Please use pnpm",
"npm": "Please use pnpm"
},
"packageManager": "pnpm@9.0.5",
"devDependencies": {
"@biomejs/biome": "^2.4.10",
"@graphql-codegen/cli": "^6.2.1",
"@graphql-codegen/typed-document-node": "^6.1.7",
"@graphql-codegen/typescript": "^5.0.9",
"@graphql-codegen/typescript-operations": "^5.0.9",
"@graphql-typed-document-node/core": "^3.2.0",
"@types/apollo-upload-client": "^19.0.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "~24",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react": "^6.0.1",
"rollup-plugin-analyzer": "^4.0.0",
"sass": "~1.77.6",
"typescript": "~6.0.2",
"vite": "^8.0.3",
"vite-plugin-graphql-loader": "^5.0.1"
},
"dependencies": {
"@apollo/client": "4.1.6",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.1",
"@hookform/lenses": "^0.9.0",
"@hookform/resolvers": "5.2.2",
"apollo-upload-client": "^19.0.0",
"bootstrap": "5.2.3",
"classnames": "^2.5.1",
"graphql": "^16.13.2",
"graphql-tag": "^2.12.6",
"i18n-iso-countries": "^7.14.0",
"lodash-es": "^4.18.1",
"p-debounce": "^5.1.0",
"query-string": "^9.3.1",
"react": "^18.3.1",
"react-bootstrap": "^2.10.10",
"react-bootstrap-typeahead": "^6.4.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-hook-form": "7.72.1",
"react-markdown": "^10.1.0",
"react-responsive-carousel": "^3.2.23",
"react-router-dom": "^6.30.2",
"react-select": "5.8.3",
"rehype-external-links": "^3.0.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"temporal-polyfill": "^0.3.2",
"yup": "1.0.1"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: frontend/src/App.scss
================================================
@import "./styles/theme";
@import "./hooks/toast";
@import "./pages/home/styles";
@import "./pages/users/styles";
@import "./pages/notifications/styles";
@import "./pages/performers/styles";
@import "./pages/scenes/styles";
@import "./pages/scenes/sceneForm/styles";
@import "./pages/studios/styles";
@import "./pages/search/search";
@import "./components/checkboxSelect/styles";
@import "./components/editCard/styles";
@import "./components/form/styles";
@import "./components/fragments/styles";
@import "./components/imageCarousel/styles";
@import "./components/imageChangeRow/styles";
@import "./components/list/styles";
@import "./components/performerCard/styles";
@import "./components/performerSelect/styles";
@import "./components/sceneCard/styles";
@import "./components/searchField/styles";
@import "./components/studioSelect/styles";
@import "./components/tagFilter/styles";
@import "./components/tagSelect/styles";
@import "./components/editImages/styles";
@import "./components/urlInput/styles";
@import "./components/image/styles";
@import "react-bootstrap-typeahead/css/Typeahead.css";
/* this is a bit of a hack, because we can't supply direct class names
to the react-select controls */
/* stylelint-disable selector-class-pattern */
.react-select__control {
background-color: $secondary;
border-color: $secondary;
color: $text-color;
cursor: pointer;
.react-select__single-value,
.react-select__input {
color: black;
}
.react-select__multi-value {
background-color: $muted-gray;
color: $text-color;
}
}
div.react-select__menu {
background-color: $secondary;
color: $text-color;
z-index: 2;
.react-select__option {
color: $text-color;
}
.react-select__option--is-focused {
background-color: #8a9ba826;
cursor: pointer;
}
}
.LoginPrompt {
height: 70vh;
width: 960px;
margin-left: auto;
margin-right: auto;
display: flex;
a {
color: $link-color;
}
}
.bullet-separator {
&::before {
content: "\2022";
margin: 0 0.5rem;
}
}
.PendingEditTab {
color: $warning !important;
}
.MainContent {
padding-top: 2rem;
padding-bottom: 1rem;
}
.NarrowPage {
margin: auto;
max-width: 1200px;
}
.NotificationBadge {
color: white;
&:active,
&:hover {
color: var(--bs-gray-500);
}
}
================================================
FILE: frontend/src/App.tsx
================================================
import type { FC } from "react";
import { ApolloProvider } from "@apollo/client/react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { config as fontAwesomeConfig } from "@fortawesome/fontawesome-svg-core";
import Main from "src/Main";
import createClient from "src/utils/createClient";
import Pages from "src/pages";
import Title from "src/components/title";
import { ToastProvider } from "src/hooks/useToast";
fontAwesomeConfig.autoAddCss = false;
import "./App.scss";
import "@fortawesome/fontawesome-svg-core/styles.css";
const client = createClient();
const App: FC = () => (
<ApolloProvider client={client}>
<BrowserRouter>
<ToastProvider>
<Routes>
<Route
path="/*"
element={
<Main>
<Title />
<Pages />
</Main>
}
/>
</Routes>
</ToastProvider>
</BrowserRouter>
</ApolloProvider>
);
export default App;
================================================
FILE: frontend/src/Login.tsx
================================================
import { type FC, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { Button, Col, Form, Row } from "react-bootstrap";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import cx from "classnames";
import { ROUTE_REGISTER, ROUTE_FORGOT_PASSWORD } from "src/constants/route";
import { getPlatformURL, getCredentialsSetting } from "src/utils/createClient";
import "./App.scss";
import { useCurrentUser } from "./hooks";
const schema = yup.object({
username: yup.string().required("Username is required"),
password: yup.string().required("Password is required"),
});
type LoginFormData = yup.InferType<typeof schema>;
const Messages: Record<string, string> = {
"password-reset": "Password successfully reset",
"account-created": "Account successfully created",
};
const Login: FC = () => {
const [loading, setLoading] = useState(false);
const location = useLocation();
const navigate = useNavigate();
const [loginError, setLoginError] = useState("");
const msg = new URLSearchParams(location.search).get("msg");
const redirect = new URLSearchParams(location.search).get("redirect");
const { isAuthenticated } = useCurrentUser();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginFormData>({
resolver: yupResolver(schema),
});
if (isAuthenticated) navigate("/");
const onSubmit = async (formData: LoginFormData) => {
setLoading(true);
const body = new FormData();
body.append("username", formData.username);
body.append("password", formData.password);
const res = await fetch(`${getPlatformURL()}login`, {
method: "POST",
body,
credentials: getCredentialsSetting(),
}).finally(() => setLoading(false));
const returnURL = decodeURIComponent(redirect ?? "") || "/";
if (res.ok) window.location.replace(returnURL);
else setLoginError("Access denied");
};
return (
<div className="LoginPrompt">
<Form
className="align-self-center col-4 mx-auto"
onSubmit={handleSubmit(onSubmit)}
>
<Form.Floating>
<Form.Control
className={cx({ "is-invalid": errors?.username })}
placeholder="Username"
{...register("username")}
/>
<Form.Label>Username</Form.Label>
<div className="invalid-feedback text-end">
{errors?.username?.message}
</div>
</Form.Floating>
<Form.Floating className="my-3">
<Form.Control
type="password"
className={cx({ "is-invalid": errors?.password })}
placeholder="Password"
{...register("password")}
/>
<Form.Label>Password</Form.Label>
<div className="invalid-feedback text-end">
{errors?.password?.message}
</div>
</Form.Floating>
<Row>
<Col xs={9}>
<div>
<Link to={ROUTE_REGISTER}>
<small>Register</small>
</Link>
</div>
<div>
<Link to={ROUTE_FORGOT_PASSWORD}>
<small>Forgot Password</small>
</Link>
</div>
</Col>
<Col xs={3} className="d-flex justify-content-end">
<div>
<Button type="submit" className="login-button" disabled={loading}>
Login
</Button>
</div>
</Col>
</Row>
<Row>
<p className="col text-end text-danger">{loginError}</p>
</Row>
<Row>
<p className="col text-end text-success">
{Messages[msg ?? ""] ?? ""}
</p>
</Row>
</Form>
</div>
);
};
export default Login;
================================================
FILE: frontend/src/Main.tsx
================================================
import { type FC, useEffect } from "react";
import { Navbar, Nav, Button, Badge } from "react-bootstrap";
import { NavLink, useLocation, useNavigate, Link } from "react-router-dom";
import { faBell, faBook, faUser } from "@fortawesome/free-solid-svg-icons";
import { faBell as faBellOutlined } from "@fortawesome/free-regular-svg-icons";
import SearchField, { SearchType } from "src/components/searchField";
import { getPlatformURL, getCredentialsSetting } from "src/utils/createClient";
import { userHref, setCachedUser, canEdit, isAdmin } from "src/utils";
import { useAuth } from "src/hooks";
import { Icon } from "src/components/fragments";
import { useConfig, useUnreadNotificationsCount } from "src/graphql";
import {
ROUTE_SCENES,
ROUTE_PERFORMERS,
ROUTE_TAGS,
ROUTE_STUDIOS,
ROUTE_EDITS,
ROUTE_LOGOUT,
ROUTE_LOGIN,
ROUTE_USERS,
ROUTE_ACTIVATE,
ROUTE_RESET_PASSWORD,
ROUTE_HOME,
ROUTE_REGISTER,
ROUTE_FORGOT_PASSWORD,
ROUTE_SITES,
ROUTE_DRAFTS,
ROUTE_NOTIFICATIONS,
ROUTE_AUDITS,
} from "src/constants/route";
import AuthContext from "./context";
interface Props {
children?: React.ReactNode;
}
const Main: FC<Props> = ({ children }) => {
const location = useLocation();
const navigate = useNavigate();
const { loading, user } = useAuth();
const { data: unreadNotifications } = useUnreadNotificationsCount();
const notificationCount =
unreadNotifications?.getUnreadNotificationCount || null;
const { data: configData } = useConfig();
const guidelinesURL = configData?.getConfig.guidelines_url;
useEffect(() => {
if (loading || user) return;
if (
location.pathname !== ROUTE_ACTIVATE &&
location.pathname !== ROUTE_REGISTER &&
location.pathname !== ROUTE_LOGIN &&
location.pathname !== ROUTE_FORGOT_PASSWORD &&
location.pathname !== ROUTE_RESET_PASSWORD
) {
const redirect =
location.pathname === "/"
? ""
: `?redirect=${encodeURIComponent(location.pathname)}`;
navigate(`${ROUTE_LOGIN}${redirect}`);
}
}, [loading, user, location, navigate]);
const contextValue = {
authenticated: user !== undefined,
user,
};
if (!contextValue.authenticated)
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
const handleLogout = async () => {
const res = await fetch(`${getPlatformURL()}logout`, {
credentials: getCredentialsSetting(),
});
setCachedUser();
if (res.ok) window.location.href = ROUTE_LOGIN;
return false;
};
const renderUserNav = () =>
contextValue.authenticated &&
contextValue.user && (
<>
<Link to={ROUTE_NOTIFICATIONS}>
<Button variant="link" className="NotificationBadge">
<Icon icon={notificationCount ? faBell : faBellOutlined} />
{notificationCount && (
<Badge bg="danger" className="ms-1">
{notificationCount}
</Badge>
)}
</Button>
</Link>
<NavLink
to={userHref(contextValue.user)}
className="nav-link ms-auto me-2"
>
<Icon icon={faUser} className="me-2" />
{contextValue.user.name}
</NavLink>
{isAdmin(user) && (
<NavLink to={ROUTE_USERS} className="nav-link">
Users
</NavLink>
)}
<NavLink
to={ROUTE_LOGOUT}
onClick={handleLogout}
className="nav-link me-4"
>
Logout
</NavLink>
</>
);
return (
<div>
<Navbar bg="dark" variant="dark" className="px-4">
<Nav className="me-auto">
<NavLink to={ROUTE_HOME} className="nav-link">
Home
</NavLink>
<NavLink to={ROUTE_PERFORMERS} className="nav-link">
Performers
</NavLink>
<NavLink to={ROUTE_SCENES} className="nav-link">
Scenes
</NavLink>
<NavLink to={ROUTE_STUDIOS} className="nav-link">
Studios
</NavLink>
<NavLink to={ROUTE_TAGS} className="nav-link">
Tags
</NavLink>
<NavLink to={ROUTE_EDITS} className="nav-link">
Edits
</NavLink>
{canEdit(user) && (
<NavLink to={ROUTE_DRAFTS} className="nav-link">
Drafts
</NavLink>
)}
{isAdmin(user) && (
<>
<NavLink to={ROUTE_SITES} className="nav-link">
Sites
</NavLink>
<NavLink to={ROUTE_AUDITS} className="nav-link">
Audits
</NavLink>
</>
)}
{guidelinesURL && (
<a
href={guidelinesURL}
target="_blank"
rel="noopener noreferrer"
className="nav-link"
>
<Icon icon={faBook} className="mx-2" />
Guidelines
</a>
)}
</Nav>
<Nav className="align-items-center">
{contextValue.authenticated && renderUserNav()}
<SearchField searchType={SearchType.Combined} nav showAllLink />
</Nav>
</Navbar>
<AuthContext.Provider value={contextValue}>
<main className="MainContent container-fluid">{children}</main>
</AuthContext.Provider>
</div>
);
};
export default Main;
================================================
FILE: frontend/src/components/amendableEditCard/AmendableChangeRow.tsx
================================================
import type { FC } from "react";
import { Col, Row, Button } from "react-bootstrap";
import { faXmark, faUndo } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import { Icon } from "src/components/fragments";
import { useAmendment } from "./AmendmentContext";
export interface AmendableChangeRowProps {
name?: string;
field: string;
newValue?: string | number | null;
oldValue?: string | number | null;
showDiff?: boolean;
}
const AmendableChangeRow: FC<AmendableChangeRowProps> = ({
name,
field,
newValue,
oldValue,
showDiff = false,
}) => {
const { state, clearField, restoreField } = useAmendment();
const isRemoved = state.removedFields.has(field);
if (!name || (!newValue && !oldValue)) return null;
return (
<Row
className={cx("mb-2", {
"opacity-50 text-decoration-line-through": isRemoved,
})}
>
<b className="col-2 text-end pt-1">{name}</b>
{showDiff && (
<Col xs={4}>
<div className="EditDiff bg-danger">{oldValue}</div>
</Col>
)}
<Col xs={showDiff ? 4 : 8}>
<div className={cx("EditDiff", { "bg-success": showDiff })}>
{newValue}
</div>
</Col>
<Col xs={2} className="text-end">
{!isRemoved && (
<Button
variant="danger"
size="sm"
onClick={() => clearField(field)}
title={`Remove ${name} change`}
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
onClick={() => restoreField(field)}
title={`Restore ${name} change`}
>
<Icon icon={faUndo} />
</Button>
)}
</Col>
</Row>
);
};
export default AmendableChangeRow;
================================================
FILE: frontend/src/components/amendableEditCard/AmendableImageChangeRow.tsx
================================================
import type { FC } from "react";
import { Col, Row, Button } from "react-bootstrap";
import { faXmark, faUndo } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import ImageComponent from "src/components/image";
import { Icon } from "src/components/fragments";
import { useAmendment } from "./AmendmentContext";
type Image = {
height: number;
id: string;
url: string;
width: number;
};
const CLASSNAME = "ImageChangeRow";
const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
export interface AmendableImageChangeRowProps {
field: string;
newImages?: (Image | null)[] | null;
oldImages?: (Image | null)[] | null;
showDiff?: boolean;
}
const AmendableImageChangeRow: FC<AmendableImageChangeRowProps> = ({
field,
newImages,
oldImages,
showDiff = false,
}) => {
const {
state,
clearAddedItem,
clearRemovedItem,
restoreAddedItem,
restoreRemovedItem,
} = useAmendment();
const removedAddedIndices = state.removedAddedItems.get(field);
const removedRemovedIndices = state.removedRemovedItems.get(field);
if ((newImages ?? []).length === 0 && (oldImages ?? []).length === 0)
return null;
return (
<Row className={CLASSNAME}>
<b className="col-2 text-end">Images</b>
{showDiff && (
<Col xs={4}>
{(oldImages ?? []).length > 0 && (
<>
<h6>Removed</h6>
<div className={CLASSNAME}>
{(oldImages ?? []).map((image, index) => {
const isRemoved = removedRemovedIndices?.has(index);
return (
<div
key={image?.id ?? `deleted-${index}`}
className={cx("d-flex align-items-start mb-2", {
"opacity-50": isRemoved,
})}
>
{image === null ? (
<img className={CLASSNAME_IMAGE} alt="Deleted" />
) : (
<div className={CLASSNAME_IMAGE}>
<ImageComponent images={image} alt="" size="full" />
<div className="text-center">
{image.width} x {image.height}
</div>
</div>
)}
<div className="ms-2">
{!isRemoved && (
<Button
variant="danger"
size="sm"
onClick={() => clearRemovedItem(field, index)}
title="Remove this image change from the edit"
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
onClick={() => restoreRemovedItem(field, index)}
title="Restore this image"
>
<Icon icon={faUndo} />
</Button>
)}
</div>
</div>
);
})}
</div>
</>
)}
</Col>
)}
<Col xs={showDiff ? 4 : 8}>
{(newImages ?? []).length > 0 && (
<>
{showDiff && <h6>Added</h6>}
<div className={CLASSNAME}>
{(newImages ?? []).map((image, index) => {
const isRemoved = removedAddedIndices?.has(index);
return (
<div
key={image?.id ?? `deleted-${index}`}
className={cx("d-flex align-items-start mb-2", {
"opacity-50": isRemoved,
})}
>
{image === null ? (
<img className={CLASSNAME_IMAGE} alt="Deleted" />
) : (
<div className={CLASSNAME_IMAGE}>
<ImageComponent images={image} alt="" size="full" />
<div className="text-center">
{image.width} x {image.height}
</div>
</div>
)}
<div className="ms-2">
{!isRemoved && (
<Button
variant="danger"
size="sm"
onClick={() => clearAddedItem(field, index)}
title="Remove this image from the edit"
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
onClick={() => restoreAddedItem(field, index)}
title="Restore this image"
>
<Icon icon={faUndo} />
</Button>
)}
</div>
</div>
);
})}
</div>
</>
)}
</Col>
<Col xs={2} />
</Row>
);
};
export default AmendableImageChangeRow;
================================================
FILE: frontend/src/components/amendableEditCard/AmendableLinkedChangeRow.tsx
================================================
import type { FC } from "react";
import { Link } from "react-router-dom";
import { Col, Row, Button } from "react-bootstrap";
import { faXmark, faUndo } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import { Icon } from "src/components/fragments";
import { useAmendment } from "./AmendmentContext";
interface Change {
name: string | null | undefined;
link: string | null | undefined;
}
interface AmendableLinkedChangeRowProps {
name: string;
field: string;
oldEntity?: Change | null;
newEntity?: Change | null;
showDiff?: boolean;
}
const AmendableLinkedChangeRow: FC<AmendableLinkedChangeRowProps> = ({
name,
field,
newEntity,
oldEntity,
showDiff = false,
}) => {
const { state, clearField, restoreField } = useAmendment();
const isRemoved = state.removedFields.has(field);
function getValue(value: Change | null | undefined) {
if (!value?.name) {
return;
}
if (!value.link) {
return value.name;
}
return <Link to={value.link}>{value.name}</Link>;
}
if (!newEntity?.link && !oldEntity?.link) return null;
return (
<Row
className={cx("mb-2", {
"opacity-50 text-decoration-line-through": isRemoved,
})}
>
<b className="col-2 text-end pt-1">{name}</b>
{showDiff && (
<Col xs={4} className="ms-auto" key={oldEntity?.name}>
<div className="EditDiff bg-danger">{getValue(oldEntity)}</div>
</Col>
)}
<Col xs={showDiff ? 4 : 8} key={newEntity?.name}>
<div className={cx("EditDiff", { "bg-success": showDiff })}>
{getValue(newEntity)}
</div>
</Col>
<Col xs={2} className="text-end">
{!isRemoved && (
<Button
variant="danger"
size="sm"
onClick={() => clearField(field)}
title={`Remove ${name} change`}
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
onClick={() => restoreField(field)}
title={`Restore ${name} change`}
>
<Icon icon={faUndo} />
</Button>
)}
</Col>
</Row>
);
};
export default AmendableLinkedChangeRow;
================================================
FILE: frontend/src/components/amendableEditCard/AmendableListChangeRow.tsx
================================================
import type { PropsWithChildren } from "react";
import { Col, Row, Button } from "react-bootstrap";
import { faXmark, faUndo } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import { Icon } from "src/components/fragments";
import { useAmendment } from "./AmendmentContext";
interface AmendableListChangeRowProps<T> {
added?: T[] | null;
removed?: T[] | null;
renderItem: (o: T) => JSX.Element | undefined;
getKey: (o: T) => string;
name: string;
field: string;
showDiff?: boolean;
}
const CLASSNAME = "ListChangeRow";
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
const AmendableListChangeRow = <T,>({
added,
removed,
name,
field,
getKey,
renderItem,
showDiff,
}: PropsWithChildren<AmendableListChangeRowProps<T>>) => {
const {
state,
clearAddedItem,
clearRemovedItem,
restoreAddedItem,
restoreRemovedItem,
} = useAmendment();
const removedAddedIndices = state.removedAddedItems.get(field);
const removedRemovedIndices = state.removedRemovedItems.get(field);
if ((added ?? []).length === 0 && (removed ?? []).length === 0) return null;
return (
<Row className={`${CLASSNAME}-${name}`}>
<b className="col-2 text-end">{name}</b>
{showDiff && (
<Col xs={4}>
{(removed ?? []).length > 0 && (
<>
<h6>Removed</h6>
<div className={CLASSNAME}>
<ul>
{(removed ?? []).map((u, index) => {
const isRemoved = removedRemovedIndices?.has(index);
return (
<li
key={getKey(u)}
className={cx("d-flex align-items-center", {
"opacity-50 text-decoration-line-through": isRemoved,
})}
>
<span className="flex-grow-1">{renderItem(u)}</span>
{!isRemoved && (
<Button
variant="danger"
size="sm"
className="ms-2"
onClick={() => clearRemovedItem(field, index)}
title="Remove this item from the edit"
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
className="ms-2"
onClick={() => restoreRemovedItem(field, index)}
title="Restore this item"
>
<Icon icon={faUndo} />
</Button>
)}
</li>
);
})}
</ul>
</div>
</>
)}
</Col>
)}
<Col xs={showDiff ? 4 : 8}>
{(added ?? []).length > 0 && (
<>
{showDiff && <h6>Added</h6>}
<div className={CLASSNAME}>
<ul>
{(added ?? []).map((u, index) => {
const isRemoved = removedAddedIndices?.has(index);
return (
<li
key={getKey(u)}
className={cx("d-flex align-items-center", {
"opacity-50 text-decoration-line-through": isRemoved,
})}
>
<span className="flex-grow-1">{renderItem(u)}</span>
{!isRemoved && (
<Button
variant="danger"
size="sm"
className="ms-2"
onClick={() => clearAddedItem(field, index)}
title="Remove this item from the edit"
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
className="ms-2"
onClick={() => restoreAddedItem(field, index)}
title="Restore this item"
>
<Icon icon={faUndo} />
</Button>
)}
</li>
);
})}
</ul>
</div>
</>
)}
</Col>
<Col xs={2} />
</Row>
);
};
export default AmendableListChangeRow;
================================================
FILE: frontend/src/components/amendableEditCard/AmendableModifyEdit.tsx
================================================
import type { FC } from "react";
import { Col, Row, Button } from "react-bootstrap";
import {
faCheck,
faXmark,
faEdit,
faUndo,
} from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import type {
GenderEnum,
EthnicityEnum,
BreastTypeEnum,
EditFragment,
HairColorEnum,
EyeColorEnum,
} from "src/graphql";
import {
formatDuration,
getCountryByISO,
isTagEdit,
isPerformerEdit,
formatBodyModification,
isStudioEdit,
isSceneEdit,
studioHref,
categoryHref,
compareByName,
} from "src/utils";
import {
EthnicityTypes,
HairColorTypes,
EyeColorTypes,
BreastTypes,
GenderTypes,
} from "src/constants";
import { Icon } from "src/components/fragments";
import AmendableChangeRow from "./AmendableChangeRow";
import AmendableListChangeRow from "./AmendableListChangeRow";
import AmendableImageChangeRow from "./AmendableImageChangeRow";
import AmendableURLChangeRow from "./AmendableURLChangeRow";
import AmendableLinkedChangeRow from "./AmendableLinkedChangeRow";
import {
renderPerformer,
renderTag,
renderFingerprint,
} from "src/components/editCard/renderEntity";
import type {
PerformerDetails,
OldPerformerDetails,
SceneDetails,
OldSceneDetails,
StudioDetails,
OldStudioDetails,
TagDetails,
OldTagDetails,
} from "src/components/editCard/ModifyEdit";
import { useAmendment } from "./AmendmentContext";
type Details = EditFragment["details"];
type OldDetails = EditFragment["old_details"];
type Options = EditFragment["options"];
// Special component for Bra Size which combines band_size and cup_size
const AmendableBraSizeRow: FC<{
newBandSize?: number | null;
newCupSize?: string | null;
oldBandSize?: number | null;
oldCupSize?: string | null;
showDiff: boolean;
}> = ({ newBandSize, newCupSize, oldBandSize, oldCupSize, showDiff }) => {
const { state, clearField, restoreField } = useAmendment();
const isRemoved =
state.removedFields.has("band_size") || state.removedFields.has("cup_size");
const newValue = `${newBandSize || ""}${newCupSize ?? ""}`;
const oldValue = `${oldBandSize || ""}${oldCupSize ?? ""}`;
if (!newValue && !oldValue) return null;
const handleClear = () => {
clearField("band_size");
clearField("cup_size");
};
const handleRestore = () => {
restoreField("band_size");
restoreField("cup_size");
};
return (
<Row
className={cx("mb-2", {
"opacity-50 text-decoration-line-through": isRemoved,
})}
>
<b className="col-2 text-end pt-1">Bra Size</b>
{showDiff && (
<Col xs={4}>
<div className="EditDiff bg-danger">{oldValue}</div>
</Col>
)}
<Col xs={showDiff ? 4 : 8}>
<div className={cx("EditDiff", { "bg-success": showDiff })}>
{newValue}
</div>
</Col>
<Col xs={2} className="text-end">
{!isRemoved && (
<Button
variant="danger"
size="sm"
onClick={handleClear}
title="Remove Bra Size change"
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
onClick={handleRestore}
title="Restore Bra Size change"
>
<Icon icon={faUndo} />
</Button>
)}
</Col>
</Row>
);
};
const renderAmendableTagDetails = (
tagDetails: TagDetails,
oldTagDetails: OldTagDetails | undefined,
showDiff: boolean,
) => (
<>
<AmendableChangeRow
name="Name"
field="name"
newValue={tagDetails.name}
oldValue={oldTagDetails?.name}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Description"
field="description"
newValue={tagDetails.description}
oldValue={oldTagDetails?.description}
showDiff={showDiff}
/>
<AmendableLinkedChangeRow
name="Category"
field="category_id"
newEntity={{
name: tagDetails.category?.name,
link: tagDetails.category && categoryHref(tagDetails.category),
}}
oldEntity={{
name: oldTagDetails?.category?.name,
link: oldTagDetails?.category && categoryHref(oldTagDetails.category),
}}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Aliases"
field="aliases"
added={tagDetails.added_aliases?.map((a) => ({ value: a }))}
removed={tagDetails.removed_aliases?.map((a) => ({ value: a }))}
renderItem={(o) => <>{o.value}</>}
getKey={(o) => o.value}
showDiff={showDiff}
/>
</>
);
const renderAmendablePerformerDetails = (
performerDetails: PerformerDetails,
oldPerformerDetails: OldPerformerDetails | undefined,
showDiff: boolean,
setModifyAliases: boolean,
) => (
<>
{performerDetails.name && (
<AmendableChangeRow
name="Name"
field="name"
newValue={performerDetails.name}
oldValue={oldPerformerDetails?.name}
showDiff={showDiff}
/>
)}
{oldPerformerDetails?.name &&
performerDetails.name !== oldPerformerDetails.name && (
<div className="d-flex mb-2 align-items-center">
<Icon
icon={setModifyAliases ? faCheck : faXmark}
color={setModifyAliases ? "green" : "red"}
className="ms-auto"
/>
<span className="ms-2">Set performance aliases to old name</span>
</div>
)}
<AmendableChangeRow
name="Disambiguation"
field="disambiguation"
newValue={performerDetails.disambiguation}
oldValue={oldPerformerDetails?.disambiguation}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Aliases"
field="aliases"
added={performerDetails.added_aliases?.map((a) => ({ value: a }))}
removed={performerDetails.removed_aliases?.map((a) => ({ value: a }))}
renderItem={(o) => <>{o.value}</>}
getKey={(o) => o.value}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Gender"
field="gender"
newValue={
performerDetails.gender &&
GenderTypes[performerDetails.gender as keyof typeof GenderEnum]
}
oldValue={
oldPerformerDetails?.gender &&
GenderTypes[oldPerformerDetails.gender as keyof typeof GenderEnum]
}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Birthdate"
field="birthdate"
newValue={performerDetails.birthdate}
oldValue={oldPerformerDetails?.birthdate}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Deathdate"
field="deathdate"
newValue={performerDetails.deathdate}
oldValue={oldPerformerDetails?.deathdate}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Eye Color"
field="eye_color"
newValue={
performerDetails.eye_color &&
EyeColorTypes[performerDetails.eye_color as keyof typeof EyeColorEnum]
}
oldValue={
oldPerformerDetails?.eye_color &&
EyeColorTypes[
oldPerformerDetails.eye_color as keyof typeof EyeColorEnum
]
}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Hair Color"
field="hair_color"
newValue={
performerDetails.hair_color &&
HairColorTypes[
performerDetails.hair_color as keyof typeof HairColorEnum
]
}
oldValue={
oldPerformerDetails?.hair_color &&
HairColorTypes[
oldPerformerDetails.hair_color as keyof typeof HairColorEnum
]
}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Height"
field="height"
newValue={performerDetails.height}
oldValue={oldPerformerDetails?.height}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Breast Type"
field="breast_type"
newValue={
performerDetails.breast_type &&
BreastTypes[performerDetails.breast_type as keyof typeof BreastTypeEnum]
}
oldValue={
oldPerformerDetails?.breast_type &&
BreastTypes[
oldPerformerDetails.breast_type as keyof typeof BreastTypeEnum
]
}
showDiff={showDiff}
/>
<AmendableBraSizeRow
newBandSize={performerDetails.band_size}
newCupSize={performerDetails.cup_size}
oldBandSize={oldPerformerDetails?.band_size}
oldCupSize={oldPerformerDetails?.cup_size}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Waist Size"
field="waist_size"
newValue={performerDetails.waist_size}
oldValue={oldPerformerDetails?.waist_size}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Hip Size"
field="hip_size"
newValue={performerDetails.hip_size}
oldValue={oldPerformerDetails?.hip_size}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Nationality"
field="country"
newValue={getCountryByISO(performerDetails.country)}
oldValue={getCountryByISO(oldPerformerDetails?.country)}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Ethnicity"
field="ethnicity"
newValue={
performerDetails.ethnicity &&
EthnicityTypes[performerDetails.ethnicity as keyof typeof EthnicityEnum]
}
oldValue={
oldPerformerDetails?.ethnicity &&
EthnicityTypes[
oldPerformerDetails.ethnicity as keyof typeof EthnicityEnum
]
}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Career Start"
field="career_start_year"
newValue={performerDetails.career_start_year}
oldValue={oldPerformerDetails?.career_start_year}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Career End"
field="career_end_year"
newValue={performerDetails.career_end_year}
oldValue={oldPerformerDetails?.career_end_year}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Tattoos"
field="tattoos"
added={performerDetails.added_tattoos}
removed={performerDetails.removed_tattoos}
renderItem={(o) => <>{formatBodyModification(o)}</>}
getKey={(o) => `${o.location}-${o.description ?? ""}`}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Piercings"
field="piercings"
added={performerDetails.added_piercings}
removed={performerDetails.removed_piercings}
renderItem={(o) => <>{formatBodyModification(o)}</>}
getKey={(o) => `${o.location}-${o.description ?? ""}`}
showDiff={showDiff}
/>
<AmendableURLChangeRow
field="urls"
newURLs={performerDetails.added_urls}
oldURLs={performerDetails.removed_urls}
showDiff={showDiff}
/>
<AmendableImageChangeRow
field="images"
newImages={performerDetails.added_images}
oldImages={performerDetails.removed_images}
showDiff={showDiff}
/>
{performerDetails.draft_id && (
<Row className="mb-2">
<Col xs={{ offset: 2 }}>
<h6>
<Icon icon={faEdit} color="green" />
<span className="ms-1">Submitted by draft</span>
</h6>
</Col>
</Row>
)}
</>
);
const renderAmendableSceneDetails = (
sceneDetails: SceneDetails,
oldSceneDetails: OldSceneDetails | undefined,
showDiff: boolean,
) => (
<>
{sceneDetails.title && (
<AmendableChangeRow
name="Title"
field="title"
newValue={sceneDetails.title}
oldValue={oldSceneDetails?.title}
showDiff={showDiff}
/>
)}
<AmendableChangeRow
name="Date"
field="date"
newValue={sceneDetails.date}
oldValue={oldSceneDetails?.date}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Duration"
field="duration"
newValue={formatDuration(sceneDetails.duration)}
oldValue={formatDuration(oldSceneDetails?.duration)}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Performers"
field="performers"
added={sceneDetails.added_performers}
removed={sceneDetails.removed_performers}
renderItem={renderPerformer}
getKey={(o) => o.performer.id}
showDiff={showDiff}
/>
<AmendableLinkedChangeRow
name="Studio"
field="studio_id"
newEntity={{
name: sceneDetails.studio?.name,
link: sceneDetails.studio && studioHref(sceneDetails.studio),
}}
oldEntity={{
name: oldSceneDetails?.studio?.name,
link: oldSceneDetails?.studio && studioHref(oldSceneDetails.studio),
}}
showDiff={showDiff}
/>
<AmendableURLChangeRow
field="urls"
newURLs={sceneDetails.added_urls}
oldURLs={sceneDetails.removed_urls}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Details"
field="details"
newValue={sceneDetails.details}
oldValue={oldSceneDetails?.details}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Director"
field="director"
newValue={sceneDetails.director}
oldValue={oldSceneDetails?.director}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Production Date"
field="production_date"
newValue={sceneDetails.production_date}
oldValue={oldSceneDetails?.production_date}
showDiff={showDiff}
/>
<AmendableChangeRow
name="Studio Code"
field="code"
newValue={sceneDetails.code}
oldValue={oldSceneDetails?.code}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Tags"
field="tags"
added={sceneDetails.added_tags?.slice().sort(compareByName)}
removed={sceneDetails.removed_tags?.slice().sort(compareByName)}
renderItem={renderTag}
getKey={(o) => o.id}
showDiff={showDiff}
/>
<AmendableImageChangeRow
field="images"
newImages={sceneDetails?.added_images}
oldImages={sceneDetails?.removed_images}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Fingerprints"
field="fingerprints"
added={sceneDetails.added_fingerprints}
removed={sceneDetails.removed_fingerprints}
renderItem={renderFingerprint}
getKey={(o) => `${o.hash}${o.algorithm}`}
showDiff={showDiff}
/>
{sceneDetails.draft_id && (
<Row className="mb-2">
<Col xs={{ offset: 2 }}>
<h6>
<Icon icon={faEdit} color="green" />
<span className="ms-1">Submitted by draft</span>
</h6>
</Col>
</Row>
)}
</>
);
const renderAmendableStudioDetails = (
studioDetails: StudioDetails,
oldStudioDetails: OldStudioDetails | undefined,
showDiff: boolean,
) => (
<>
<AmendableChangeRow
name="Name"
field="name"
newValue={studioDetails.name}
oldValue={oldStudioDetails?.name}
showDiff={showDiff}
/>
<AmendableListChangeRow
name="Aliases"
field="aliases"
added={studioDetails.added_aliases?.map((a) => ({ value: a }))}
removed={studioDetails.removed_aliases?.map((a) => ({ value: a }))}
renderItem={(o) => <>{o.value}</>}
getKey={(o) => o.value}
showDiff={showDiff}
/>
<AmendableLinkedChangeRow
name="Network"
field="parent_id"
newEntity={{
name: studioDetails.parent?.name,
link: studioDetails.parent && studioHref(studioDetails.parent),
}}
oldEntity={{
name: oldStudioDetails?.parent?.name,
link: oldStudioDetails?.parent && studioHref(oldStudioDetails.parent),
}}
showDiff={showDiff}
/>
<AmendableURLChangeRow
field="urls"
newURLs={studioDetails.added_urls}
oldURLs={studioDetails.removed_urls}
showDiff={showDiff}
/>
<AmendableImageChangeRow
field="images"
newImages={studioDetails.added_images}
oldImages={studioDetails.removed_images}
showDiff={showDiff}
/>
</>
);
interface AmendableModifyEditProps {
details: Details | null;
oldDetails?: OldDetails | null;
options?: Options;
}
const AmendableModifyEdit: FC<AmendableModifyEditProps> = ({
details,
oldDetails,
options,
}) => {
if (!details) return null;
const showDiff = !!oldDetails;
if (isTagEdit(details) && (isTagEdit(oldDetails) || !oldDetails)) {
return renderAmendableTagDetails(
details,
oldDetails ?? undefined,
showDiff,
);
}
if (
isPerformerEdit(details) &&
(isPerformerEdit(oldDetails) || !oldDetails)
) {
return renderAmendablePerformerDetails(
details,
oldDetails ?? undefined,
showDiff,
options?.set_modify_aliases ?? false,
);
}
if (isStudioEdit(details) && (isStudioEdit(oldDetails) || !oldDetails)) {
return renderAmendableStudioDetails(
details,
oldDetails ?? undefined,
showDiff,
);
}
if (isSceneEdit(details) && (isSceneEdit(oldDetails) || !oldDetails)) {
return renderAmendableSceneDetails(
details,
oldDetails ?? undefined,
showDiff,
);
}
return null;
};
export default AmendableModifyEdit;
================================================
FILE: frontend/src/components/amendableEditCard/AmendableURLChangeRow.tsx
================================================
import type { FC } from "react";
import { Col, Row, Button } from "react-bootstrap";
import { faXmark, faUndo } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import { SiteLink, Icon } from "src/components/fragments";
import type { URL } from "src/components/urlChangeRow";
import { useAmendment } from "./AmendmentContext";
const CLASSNAME = "URLChangeRow";
interface AmendableURLChangeRowProps {
field: string;
newURLs?: URL[] | null;
oldURLs?: URL[] | null;
showDiff?: boolean;
}
const AmendableURLChangeRow: FC<AmendableURLChangeRowProps> = ({
field,
newURLs,
oldURLs,
showDiff,
}) => {
const {
state,
clearAddedItem,
clearRemovedItem,
restoreAddedItem,
restoreRemovedItem,
} = useAmendment();
const removedAddedIndices = state.removedAddedItems.get(field);
const removedRemovedIndices = state.removedRemovedItems.get(field);
if ((newURLs ?? []).length === 0 && (oldURLs ?? []).length === 0) return null;
return (
<Row className={CLASSNAME}>
<b className="col-2 text-end">Links</b>
{showDiff && (
<Col xs={4}>
{(oldURLs ?? []).length > 0 && (
<>
<h6>Removed</h6>
<div className={CLASSNAME}>
<ul className="ps-0">
{(oldURLs ?? []).map((url, index) => {
const isRemoved = removedRemovedIndices?.has(index);
return (
<li
key={url.url}
className={cx("d-flex align-items-start", {
"opacity-50 text-decoration-line-through": isRemoved,
})}
>
<SiteLink site={url.site} />
<a
href={url.url}
target="_blank"
rel="noopener noreferrer"
className="d-inline-block flex-grow-1 text-break"
>
{url.url}
</a>
{!isRemoved && (
<Button
variant="danger"
size="sm"
className="ms-2"
onClick={() => clearRemovedItem(field, index)}
title="Remove this URL change from the edit"
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
className="ms-2"
onClick={() => restoreRemovedItem(field, index)}
title="Restore this URL"
>
<Icon icon={faUndo} />
</Button>
)}
</li>
);
})}
</ul>
</div>
</>
)}
</Col>
)}
<Col xs={showDiff ? 4 : 8}>
{(newURLs ?? []).length > 0 && (
<>
{showDiff && <h6>Added</h6>}
<div className={CLASSNAME}>
<ul className="ps-0">
{(newURLs ?? []).map((url, index) => {
const isRemoved = removedAddedIndices?.has(index);
return (
<li
key={url.url}
className={cx("d-flex align-items-start", {
"opacity-50 text-decoration-line-through": isRemoved,
})}
>
<SiteLink site={url.site} />
<a
href={url.url}
target="_blank"
rel="noopener noreferrer"
className="d-inline-block flex-grow-1 text-break"
>
{url.url}
</a>
{!isRemoved && (
<Button
variant="danger"
size="sm"
className="ms-2"
onClick={() => clearAddedItem(field, index)}
title="Remove this URL from the edit"
>
<Icon icon={faXmark} />
</Button>
)}
{isRemoved && (
<Button
variant="secondary"
size="sm"
className="ms-2"
onClick={() => restoreAddedItem(field, index)}
title="Restore this URL"
>
<Icon icon={faUndo} />
</Button>
)}
</li>
);
})}
</ul>
</div>
</>
)}
</Col>
<Col xs={2} />
</Row>
);
};
export default AmendableURLChangeRow;
================================================
FILE: frontend/src/components/amendableEditCard/AmendmentContext.tsx
================================================
import {
createContext,
useContext,
useState,
useCallback,
type FC,
type ReactNode,
} from "react";
export interface AmendmentState {
removedFields: Set<string>;
removedAddedItems: Map<string, Set<number>>;
removedRemovedItems: Map<string, Set<number>>;
}
interface AmendmentContextValue {
state: AmendmentState;
clearField: (field: string) => void;
clearAddedItem: (field: string, index: number) => void;
clearRemovedItem: (field: string, index: number) => void;
restoreField: (field: string) => void;
restoreAddedItem: (field: string, index: number) => void;
restoreRemovedItem: (field: string, index: number) => void;
hasChanges: boolean;
}
const AmendmentContext = createContext<AmendmentContextValue | null>(null);
export const useAmendment = () => {
const context = useContext(AmendmentContext);
if (!context) {
throw new Error("useAmendment must be used within AmendmentProvider");
}
return context;
};
export const useAmendmentOptional = () => useContext(AmendmentContext);
interface AmendmentProviderProps {
children: ReactNode;
}
export const AmendmentProvider: FC<AmendmentProviderProps> = ({ children }) => {
const [removedFields, setRemovedFields] = useState<Set<string>>(new Set());
const [removedAddedItems, setRemovedAddedItems] = useState<
Map<string, Set<number>>
>(new Map());
const [removedRemovedItems, setRemovedRemovedItems] = useState<
Map<string, Set<number>>
>(new Map());
const clearField = useCallback((field: string) => {
setRemovedFields((prev) => {
const next = new Set(prev);
next.add(field);
return next;
});
}, []);
const clearAddedItem = useCallback((field: string, index: number) => {
setRemovedAddedItems((prev) => {
const next = new Map(prev);
const indices = next.get(field) ?? new Set<number>();
indices.add(index);
next.set(field, indices);
return next;
});
}, []);
const clearRemovedItem = useCallback((field: string, index: number) => {
setRemovedRemovedItems((prev) => {
const next = new Map(prev);
const indices = next.get(field) ?? new Set<number>();
indices.add(index);
next.set(field, indices);
return next;
});
}, []);
const restoreField = useCallback((field: string) => {
setRemovedFields((prev) => {
const next = new Set(prev);
next.delete(field);
return next;
});
}, []);
const restoreAddedItem = useCallback((field: string, index: number) => {
setRemovedAddedItems((prev) => {
const next = new Map(prev);
const indices = next.get(field);
if (indices) {
indices.delete(index);
if (indices.size === 0) {
next.delete(field);
} else {
next.set(field, indices);
}
}
return next;
});
}, []);
const restoreRemovedItem = useCallback((field: string, index: number) => {
setRemovedRemovedItems((prev) => {
const next = new Map(prev);
const indices = next.get(field);
if (indices) {
indices.delete(index);
if (indices.size === 0) {
next.delete(field);
} else {
next.set(field, indices);
}
}
return next;
});
}, []);
const hasChanges =
removedFields.size > 0 ||
Array.from(removedAddedItems.values()).some((s) => s.size > 0) ||
Array.from(removedRemovedItems.values()).some((s) => s.size > 0);
const value: AmendmentContextValue = {
state: {
removedFields,
removedAddedItems,
removedRemovedItems,
},
clearField,
clearAddedItem,
clearRemovedItem,
restoreField,
restoreAddedItem,
restoreRemovedItem,
hasChanges,
};
return (
<AmendmentContext.Provider value={value}>
{children}
</AmendmentContext.Provider>
);
};
================================================
FILE: frontend/src/components/amendableEditCard/index.ts
================================================
export { default as AmendableModifyEdit } from "./AmendableModifyEdit";
export { default as AmendableChangeRow } from "./AmendableChangeRow";
export { default as AmendableListChangeRow } from "./AmendableListChangeRow";
export { default as AmendableURLChangeRow } from "./AmendableURLChangeRow";
export { default as AmendableImageChangeRow } from "./AmendableImageChangeRow";
export { default as AmendableLinkedChangeRow } from "./AmendableLinkedChangeRow";
export {
AmendmentProvider,
useAmendment,
type AmendmentState,
} from "./AmendmentContext";
================================================
FILE: frontend/src/components/changeRow/ChangeRow.tsx
================================================
import type { FC } from "react";
import { Col, Row } from "react-bootstrap";
import cx from "classnames";
export interface ChangeRowProps {
name?: string;
newValue?: string | number | null;
oldValue?: string | number | null;
showDiff?: boolean;
}
const ChangeRow: FC<ChangeRowProps> = ({
name,
newValue,
oldValue,
showDiff = false,
}) =>
name && (newValue || oldValue) ? (
<Row className="mb-2">
<b className="col-2 text-end pt-1">{name}</b>
{showDiff && (
<Col xs={5}>
<div className="EditDiff bg-danger">{oldValue}</div>
</Col>
)}
<Col xs={showDiff ? 5 : 10}>
<div className={cx("EditDiff", { "bg-success": showDiff })}>
{newValue}
</div>
</Col>
</Row>
) : null;
export default ChangeRow;
================================================
FILE: frontend/src/components/changeRow/index.ts
================================================
export { default } from "./ChangeRow";
================================================
FILE: frontend/src/components/checkboxSelect/CheckboxSelect.tsx
================================================
// biome-ignore-all lint/correctness/noNestedComponentDefinitions: Necessary for react-select
import type { FC } from "react";
import Select, { type OnChangeValue } from "react-select";
import { Form } from "react-bootstrap";
import { uniq } from "lodash-es";
interface MultiSelectProps {
values: IOptionType[];
onChange: (values: string[]) => void;
placeholder?: string;
plural?: string;
selected?: string[];
}
interface IOptionType {
label: string;
value: string;
subValues: string[] | null;
}
const CheckboxSelect: FC<MultiSelectProps> = ({
values,
onChange,
placeholder = "Select...",
plural = "values",
selected = [],
}) => {
const handleChange = (vals: OnChangeValue<IOptionType, true>) => {
onChange(uniq(vals.flatMap((v) => [v.value, ...(v.subValues ?? [])])));
};
const formatLabel = (
option: IOptionType,
meta: { context: "menu" | "value" },
) => {
if (meta.context === "menu")
return option.subValues === null ? (
<div className="d-flex ms-3">
<Form.Check
className="me-2"
checked={selected.includes(option.value)}
/>
{option.label}
</div>
) : (
<div className="d-flex">
<Form.Check
className="me-2"
checked={selected.includes(option.value)}
/>
<span className="text-muted">{option.label}</span>
</div>
);
return `${
selected.length === 0 ? "All" : selected.length
} ${plural} selected`;
};
const selectedOptions = values.filter((val) => selected.includes(val.value));
return (
<Select
value={selectedOptions}
isMulti
classNamePrefix="react-select"
className="react-select CheckboxSelect"
options={values}
onChange={handleChange}
formatOptionLabel={formatLabel}
hideSelectedOptions={false}
closeMenuOnSelect={false}
placeholder={placeholder}
noOptionsMessage={() => null}
styles={{
option: (base) => ({
...base,
backgroundColor: "transparent",
}),
}}
components={{
DropdownIndicator: () => null,
IndicatorSeparator: () => null,
MultiValue: (e) =>
e.data.value === selected[0] ? (
<span className="text-secondary">
{selected.length} {plural} selected
</span>
) : null,
}}
/>
);
};
export default CheckboxSelect;
================================================
FILE: frontend/src/components/checkboxSelect/index.ts
================================================
import CheckboxSelect from "./CheckboxSelect";
export default CheckboxSelect;
================================================
FILE: frontend/src/components/checkboxSelect/styles.scss
================================================
.CheckboxSelect {
width: 20rem;
}
================================================
FILE: frontend/src/components/deleteButton/DeleteButton.tsx
================================================
import { type FC, useState } from "react";
import { Button } from "react-bootstrap";
import Modal from "src/components/modal";
import { useCurrentUser } from "src/hooks";
interface DeleteButtonProps {
message?: string;
onClick: () => void;
disabled?: boolean;
className?: string;
}
const DeleteButton: FC<DeleteButtonProps> = ({
message,
onClick,
disabled = false,
className,
}) => {
const { isAdmin } = useCurrentUser();
const [showDelete, setShowDelete] = useState(false);
const toggleModal = () => setShowDelete(true);
const handleDelete = (status: boolean): void => {
if (status) onClick();
setShowDelete(false);
};
const deleteModal = showDelete && (
<Modal
message={
message ??
`Are you sure you want to delete this? This operation cannot be undone.`
}
callback={handleDelete}
/>
);
return (
<>
{deleteModal}
{isAdmin && (
<Button
variant="danger"
disabled={showDelete || disabled}
onClick={toggleModal}
className={className}
>
Delete
</Button>
)}
</>
);
};
export default DeleteButton;
================================================
FILE: frontend/src/components/deleteButton/index.ts
================================================
import DeleteButton from "./DeleteButton";
export default DeleteButton;
================================================
FILE: frontend/src/components/editCard/AddComment.tsx
================================================
import { type FC, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useEditComment } from "src/graphql";
import cx from "classnames";
import { NoteInput } from "src/components/form";
import { useCurrentUser } from "src/hooks";
import { CombinedGraphQLErrors } from "@apollo/client";
interface IProps {
editID: string;
}
const AddComment: FC<IProps> = ({ editID }) => {
const { isEditor } = useCurrentUser();
const [showInput, setShowInput] = useState(false);
const [error, setError] = useState<string>();
const [comment, setComment] = useState("");
const [saveComment, { loading: saving }] = useEditComment();
if (!showInput)
return (
<div className="d-flex">
{!showInput && isEditor && (
<Button
className="ms-auto minimal"
variant="link"
onClick={() => setShowInput(true)}
>
Add Comment
</Button>
)}
</div>
);
const handleSaveComment = async () => {
const text = comment.trim();
if (text) {
const res = await saveComment({
variables: { input: { id: editID, comment: text } },
});
if (CombinedGraphQLErrors.is(res.error)) {
setError(res.error.message);
} else {
setShowInput(false);
setError("");
}
}
};
return (
<Form.Group className="mb-3">
<NoteInput
className={cx({ "is-invalid": error })}
onChange={(text) => setComment(text)}
/>
<Form.Control.Feedback type="invalid" className="text-end">
{error}
</Form.Control.Feedback>
<div className="d-flex mt-2">
<Button
variant="secondary"
className="ms-auto"
onClick={() => setShowInput(false)}
>
Cancel
</Button>
<Button
variant="primary"
className="ms-2"
disabled={saving || !comment.trim()}
onClick={handleSaveComment}
>
Save
</Button>
</div>
</Form.Group>
);
};
export default AddComment;
================================================
FILE: frontend/src/components/editCard/EditCard.tsx
================================================
import type { FC } from "react";
import { Card, Col, Row } from "react-bootstrap";
import { Link } from "react-router-dom";
import cx from "classnames";
import { faRobot } from "@fortawesome/free-solid-svg-icons";
import { Icon, Tooltip } from "src/components/fragments";
import { OperationEnum, type EditFragment } from "src/graphql";
import { formatDateTime, editHref, userHref, formatOrdinals } from "src/utils";
import ModifyEdit from "./ModifyEdit";
import EditComment from "./EditComment";
import EditHeader from "./EditHeader";
import AddComment from "./AddComment";
import VoteBar from "./VoteBar";
import EditExpiration from "./EditExpiration";
import EditStatus from "./EditStatus";
import Votes from "./Votes";
const CLASSNAME = "EditCard";
interface Props {
edit: EditFragment;
showVotes?: boolean;
showVoteBar?: boolean;
hideDiff?: boolean;
}
const EditCardComponent: FC<Props> = ({
edit,
showVotes = false,
showVoteBar = true,
hideDiff = false,
}) => {
const title = `${edit.operation.toLowerCase()} ${edit.target_type.toLowerCase()}`;
const created = new Date(edit.created);
const creation = edit.operation === OperationEnum.CREATE && (
<ModifyEdit details={edit.details} />
);
const modifications = edit.operation !== OperationEnum.CREATE && (
<ModifyEdit
details={edit.details}
oldDetails={edit.old_details}
options={edit.options ?? undefined}
/>
);
const comments = (edit.comments ?? []).map((comment) => (
<EditComment {...comment} key={comment.id} />
));
return (
<Card className={cx(CLASSNAME, "mb-3")}>
<Card.Header className="row">
<div className="flex-column col-4">
<Link to={editHref(edit)}>
<h5 className="text-capitalize">{title.toLowerCase()}</h5>
</Link>
<div>
<b className="me-2">Author:</b>
{edit.user ? (
<Link to={userHref(edit.user)}>
<span>{edit.user.name}</span>
</Link>
) : (
<span>Deleted User</span>
)}
{edit.bot && (
<Tooltip
text="Edit submitted by an automated script"
delay={50}
placement="auto"
>
<span>
<Icon icon={faRobot} className="ms-2" />
</span>
</Tooltip>
)}
</div>
<div>
<b className="me-2">Created:</b>
<span>{formatDateTime(created)}</span>
</div>
{edit.updated && edit.update_count > 0 && (
<div>
<b className="me-2">Updated:</b>
<span>{formatDateTime(edit.updated)}</span>
<small className="text-muted align-text-top ms-2">{`${formatOrdinals(edit.update_count)} revision`}</small>
</div>
)}
</div>
<div className="flex-column col-4 ms-auto text-end">
<div>
<b className="me-2">Status:</b>
<EditStatus {...edit} />
<EditExpiration edit={edit} />
{showVoteBar && <VoteBar edit={edit} />}
</div>
</div>
</Card.Header>
<hr />
<Card.Body>
<EditHeader edit={edit} compact={hideDiff} />
{!hideDiff ? (
<>
{creation}
{modifications}
<Row className="mt-2">
<Col md={{ offset: 4, span: 8 }}>
{showVotes && <Votes edit={edit} />}
{comments}
<AddComment editID={edit.id} />
</Col>
</Row>
</>
) : (
showVotes && <Votes edit={edit} />
)}
</Card.Body>
</Card>
);
};
export default EditCardComponent;
================================================
FILE: frontend/src/components/editCard/EditComment.tsx
================================================
import type { FC } from "react";
import { Card } from "react-bootstrap";
import { Link } from "react-router-dom";
import { formatDateTime, userHref, Markdown } from "src/utils";
const CLASSNAME = "EditComment";
interface Props {
id: string;
comment: string;
date: string;
user?: { name: string; id: string } | null;
}
const EditComment: FC<Props> = ({ id, comment, date, user }) => (
<Card className={CLASSNAME}>
<Card.Body className="pb-0">
<Markdown text={comment} unique={id} />
</Card.Body>
<Card.Footer className="text-end">
{user ? (
<Link to={userHref(user)}>{user.name}</Link>
) : (
<span>Deleted User</span>
)}
<span className="mx-1">•</span>
<span>{formatDateTime(date, false)}</span>
</Card.Footer>
</Card>
);
export default EditComment;
================================================
FILE: frontend/src/components/editCard/EditExpiration.tsx
================================================
import type { FC } from "react";
import type { Temporal } from "temporal-polyfill";
import {
formatDistance,
parseInstant,
isInstantInFuture,
formatInstant,
} from "src/utils";
import { Tooltip } from "src/components/fragments";
import { useConfig, VoteStatusEnum, type EditFragment } from "src/graphql";
interface Props {
edit: EditFragment;
}
const TooltipMessage: FC<{
pass: boolean;
time: Temporal.Instant | undefined;
}> = ({ pass, time }) => (
<span>
If no other votes are cast the edit will{" "}
<b className={pass ? "text-success" : "text-danger"}>
{pass ? "pass" : "fail"}
</b>{" "}
at {time ? formatInstant(time) : ""}
</span>
);
const ExpirationNotification: FC<Props> = ({ edit }) => {
const { data } = useConfig();
const config = data?.getConfig;
if (
!config?.vote_cron_interval ||
edit.status !== VoteStatusEnum.PENDING ||
!edit.expires
)
return null;
// Pending edits that have reached the voting threshold have shorter voting periods.
// This will happen for destructive edits, or when votes are not unanimous.
const shortVotingPeriod =
config.vote_application_threshold > 0 &&
edit.vote_count >= config.vote_application_threshold;
const expirationTime = parseInstant(edit.expires);
const expirationDistance =
expirationTime && isInstantInFuture(expirationTime)
? formatDistance(expirationTime)
: "in a moment";
const threshold = edit.destructive ? 1 : 0;
const pass = shortVotingPeriod || edit.vote_count >= threshold;
return (
<div>
<Tooltip
delay={0}
text={<TooltipMessage pass={pass} time={expirationTime} />}
>
<span>
Voting closes{" "}
<b>
<u>{expirationDistance}</u>
</b>
</span>
</Tooltip>
</div>
);
};
export default ExpirationNotification;
================================================
FILE: frontend/src/components/editCard/EditHeader.tsx
================================================
import { type FC, useMemo } from "react";
import { Link } from "react-router-dom";
import { Col, Row } from "react-bootstrap";
import { faCheck, faXmark, faVideo } from "@fortawesome/free-solid-svg-icons";
import { OperationEnum, type EditFragment } from "src/graphql";
import {
getEditTargetRoute,
isPerformer,
isScene,
isSceneEdit,
performerHref,
studioHref,
getEditTargetName,
} from "src/utils";
import { Icon } from "src/components/fragments";
type Target = NonNullable<EditFragment["target"]>;
const renderTargetLink = (obj?: Target | null) => {
if (!obj) return null;
if (isPerformer(obj)) {
return (
<Link to={performerHref(obj)}>
{obj.name}
{obj.disambiguation && (
<small className="text-muted ms-1">({obj.disambiguation})</small>
)}
</Link>
);
} else {
return <Link to={getEditTargetRoute(obj)}>{getEditTargetName(obj)}</Link>;
}
};
const renderTargetAddendum = (obj?: Target | null) => {
if (isScene(obj) && obj?.studio)
return (
<>
<span className="mx-2">•</span>
<Icon icon={faVideo} className="me-1" />
<Link to={studioHref(obj.studio)}>{obj.studio.name}</Link>
</>
);
return null;
};
interface EditHeaderProps {
edit: EditFragment;
compact?: boolean;
}
const EditHeader: FC<EditHeaderProps> = ({ edit, compact = false }) => {
const header = useMemo(() => {
switch (edit.operation) {
case OperationEnum.MODIFY:
if (!edit.target) return null;
return (
<>
<Col xs={2} className="fw-bold text-end">
Modifying {edit.target_type.toLowerCase()}
</Col>
<Col>
{renderTargetLink(edit.target)}
{renderTargetAddendum(edit.target)}
</Col>
</>
);
case OperationEnum.CREATE:
if (edit.applied) {
return (
<>
<Col xs={2} className="fw-bold text-end">
Created {edit.target_type.toLowerCase()}
</Col>
<Col className="ps-3">
{renderTargetLink(edit.target)}
{renderTargetAddendum(edit.target)}
</Col>
</>
);
}
// For unapplied CREATE edits, show scene info from details if available
if (compact && isSceneEdit(edit.details) && edit.details.title) {
return (
<>
<Col xs={2} className="fw-bold text-end">
Creating {edit.target_type.toLowerCase()}
</Col>
<Col className="ps-3">
<span>{edit.details.title}</span>
{edit.details.studio && (
<>
<span className="mx-2">•</span>
<Icon icon={faVideo} className="me-1" />
<Link to={studioHref(edit.details.studio)}>
{edit.details.studio.name}
</Link>
</>
)}
</Col>
</>
);
}
return null;
case OperationEnum.MERGE:
if (!edit.target) return null;
return (
<Col className="lh-base">
<Row>
<Col xs={2} className="fw-bold text-end">
Merge
</Col>
<Col xs={10}>
{edit.merge_sources?.map((target) => (
<div key={target.id}>
{renderTargetLink(target)}
{renderTargetAddendum(edit.target)}
</div>
))}
</Col>
</Row>
<Row>
<Col xs={2} className="fw-bold text-end">
Into
</Col>
<Col xs={10}>
{renderTargetLink(edit.target)}
{renderTargetAddendum(edit.target)}
</Col>
</Row>
{isPerformer(edit.target) && (
<Row>
<div className="offset-2 d-flex align-items-center">
<Icon
icon={edit.options?.set_merge_aliases ? faCheck : faXmark}
color={edit.options?.set_merge_aliases ? "green" : "red"}
/>
<span className="ms-1">
Set performance aliases to old name
</span>
</div>
</Row>
)}
</Col>
);
case OperationEnum.DESTROY:
if (!edit.target) return null;
return (
<>
<Col xs={2} className="fw-bold text-end">
Deleting
</Col>
<Col>
<span className="EditDiff bg-danger">
{renderTargetLink(edit.target)}
</span>
{renderTargetAddendum(edit.target)}
</Col>
</>
);
}
}, [edit, compact]);
return header ? <Row className="mb-4">{header}</Row> : null;
};
export default EditHeader;
================================================
FILE: frontend/src/components/editCard/EditStatus.tsx
================================================
import type { FC } from "react";
import { Badge, type BadgeProps } from "react-bootstrap";
import { VoteStatusEnum } from "src/graphql";
import { EditStatusTypes } from "src/constants/enums";
import { Tooltip } from "src/components/fragments";
import { formatDateTime } from "src/utils";
interface Props {
status: VoteStatusEnum;
closed?: string | null;
}
const EditStatus: FC<Props> = ({ closed, status }) => {
let editVariant: BadgeProps["bg"] = "warning";
if (
status === VoteStatusEnum.REJECTED ||
status === VoteStatusEnum.IMMEDIATE_REJECTED ||
status === VoteStatusEnum.FAILED ||
status === VoteStatusEnum.CANCELED
)
editVariant = "danger";
else if (
status === VoteStatusEnum.ACCEPTED ||
status === VoteStatusEnum.IMMEDIATE_ACCEPTED
)
editVariant = "success";
let tooltip = "";
switch (status) {
case VoteStatusEnum.REJECTED:
tooltip = "Edit did not get sufficient votes to pass.";
break;
case VoteStatusEnum.CANCELED:
tooltip = "Edit was cancelled by the editor.";
break;
case VoteStatusEnum.IMMEDIATE_REJECTED:
tooltip = "Edit was cancelled by an admin.";
break;
case VoteStatusEnum.FAILED:
tooltip =
"Edit application failed due to an error. See edit note for more details.";
break;
}
const tooltipContent =
closed || tooltip ? (
<>
{closed && (
<div>
Closed <b>{formatDateTime(closed)}</b>
</div>
)}
{tooltip}
</>
) : (
""
);
return (
<Tooltip text={tooltipContent}>
<Badge className="text-uppercase" bg={editVariant}>
{EditStatusTypes[status]}
</Badge>
</Tooltip>
);
};
export default EditStatus;
================================================
FILE: frontend/src/components/editCard/ModifyEdit.tsx
================================================
import type { FC } from "react";
import { Col, Row } from "react-bootstrap";
import { faCheck, faXmark, faEdit } from "@fortawesome/free-solid-svg-icons";
import type {
FingerprintAlgorithm,
PerformerFragment,
GenderEnum,
EthnicityEnum,
BreastTypeEnum,
EditFragment,
HairColorEnum,
EyeColorEnum,
} from "src/graphql";
import {
formatDuration,
getCountryByISO,
isTagEdit,
isPerformerEdit,
formatBodyModification,
isStudioEdit,
isSceneEdit,
studioHref,
categoryHref,
compareByName,
} from "src/utils";
import {
EthnicityTypes,
HairColorTypes,
EyeColorTypes,
BreastTypes,
GenderTypes,
} from "src/constants";
import { Icon } from "src/components/fragments";
import ChangeRow from "src/components/changeRow";
import ImageChangeRow from "src/components/imageChangeRow";
import URLChangeRow, { type URL } from "src/components/urlChangeRow";
import LinkedChangeRow from "../linkedChangeRow";
import ListChangeRow from "../listChangeRow";
import { renderPerformer, renderTag, renderFingerprint } from "./renderEntity";
type Details = EditFragment["details"];
type OldDetails = EditFragment["old_details"];
type Options = EditFragment["options"];
export type Image = {
height: number;
id: string;
url: string;
width: number;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type StartingWith<T, K extends string> = T extends `${K}${infer _}` ? T : never;
export type TargetOldDetails<T> = Omit<
T,
StartingWith<keyof T, "added_" | "removed_"> | "draft_id"
>;
export interface TagDetails {
name?: string | null;
description?: string | null;
category?: { id: string; name: string } | null;
added_aliases?: string[] | null;
removed_aliases?: string[] | null;
}
export type OldTagDetails = TargetOldDetails<TagDetails>;
export const renderTagDetails = (
tagDetails: TagDetails,
oldTagDetails: OldTagDetails | undefined,
showDiff: boolean,
) => (
<>
<ChangeRow
name="Name"
newValue={tagDetails.name}
oldValue={oldTagDetails?.name}
showDiff={showDiff}
/>
<ChangeRow
name="Description"
newValue={tagDetails.description}
oldValue={oldTagDetails?.description}
showDiff={showDiff}
/>
<LinkedChangeRow
name="Category"
newEntity={{
name: tagDetails.category?.name,
link: tagDetails.category && categoryHref(tagDetails.category),
}}
oldEntity={{
name: oldTagDetails?.category?.name,
link: oldTagDetails?.category && categoryHref(oldTagDetails.category),
}}
showDiff={showDiff}
/>
<ChangeRow
name="Aliases"
newValue={tagDetails.added_aliases?.join(", ")}
oldValue={tagDetails.removed_aliases?.join(", ")}
showDiff={showDiff}
/>
</>
);
export type BodyMod = {
location: string;
description?: string | null;
};
export interface PerformerDetails {
name?: string | null;
gender?: GenderEnum | null;
disambiguation?: string | null;
birthdate?: string | null;
deathdate?: string | null;
career_start_year?: number | null;
career_end_year?: number | null;
height?: number | null;
band_size?: number | null;
cup_size?: string | null;
waist_size?: number | null;
hip_size?: number | null;
breast_type?: BreastTypeEnum | null;
country?: string | null;
ethnicity?: EthnicityEnum | null;
eye_color?: string | null;
hair_color?: string | null;
added_tattoos?: BodyMod[] | null;
removed_tattoos?: BodyMod[] | null;
added_piercings?: BodyMod[] | null;
removed_piercings?: BodyMod[] | null;
added_aliases?: string[] | null;
removed_aliases?: string[] | null;
added_images?: (Image | null)[] | null;
removed_images?: (Image | null)[] | null;
added_urls?: URL[] | null;
removed_urls?: URL[] | null;
draft_id?: string | null;
}
export type OldPerformerDetails = TargetOldDetails<PerformerDetails>;
export const renderPerformerDetails = (
performerDetails: PerformerDetails,
oldPerformerDetails: OldPerformerDetails | undefined,
showDiff: boolean,
setModifyAliases = false,
) => (
<>
{performerDetails.name && (
<ChangeRow
name="Name"
newValue={performerDetails.name}
oldValue={oldPerformerDetails?.name}
showDiff={showDiff}
/>
)}
{oldPerformerDetails?.name &&
performerDetails.name !== oldPerformerDetails.name && (
<div className="d-flex mb-2 align-items-center">
<Icon
icon={setModifyAliases ? faCheck : faXmark}
color={setModifyAliases ? "green" : "red"}
className="ms-auto"
/>
<span className="ms-2">Set performance aliases to old name</span>
</div>
)}
<ChangeRow
name="Disambiguation"
newValue={performerDetails.disambiguation}
oldValue={oldPerformerDetails?.disambiguation}
showDiff={showDiff}
/>
<ChangeRow
name="Aliases"
newValue={performerDetails.added_aliases?.join(", ")}
oldValue={performerDetails.removed_aliases?.join(", ")}
showDiff={showDiff}
/>
<ChangeRow
name="Gender"
newValue={
performerDetails.gender &&
GenderTypes[performerDetails.gender as keyof typeof GenderEnum]
}
oldValue={
oldPerformerDetails?.gender &&
GenderTypes[oldPerformerDetails.gender as keyof typeof GenderEnum]
}
showDiff={showDiff}
/>
<ChangeRow
name="Birthdate"
newValue={performerDetails.birthdate}
oldValue={oldPerformerDetails?.birthdate}
showDiff={showDiff}
/>
<ChangeRow
name="Deathdate"
newValue={performerDetails.deathdate}
oldValue={oldPerformerDetails?.deathdate}
showDiff={showDiff}
/>
<ChangeRow
name="Eye Color"
newValue={
performerDetails.eye_color &&
EyeColorTypes[performerDetails.eye_color as keyof typeof EyeColorEnum]
}
oldValue={
oldPerformerDetails?.eye_color &&
EyeColorTypes[
oldPerformerDetails.eye_color as keyof typeof EyeColorEnum
]
}
showDiff={showDiff}
/>
<ChangeRow
name="Hair Color"
newValue={
performerDetails.hair_color &&
HairColorTypes[
performerDetails.hair_color as keyof typeof HairColorEnum
]
}
oldValue={
oldPerformerDetails?.hair_color &&
HairColorTypes[
oldPerformerDetails.hair_color as keyof typeof HairColorEnum
]
}
showDiff={showDiff}
/>
<ChangeRow
name="Height"
newValue={performerDetails.height}
oldValue={oldPerformerDetails?.height}
showDiff={showDiff}
/>
<ChangeRow
name="Breast Type"
newValue={
performerDetails.breast_type &&
BreastTypes[performerDetails.breast_type as keyof typeof BreastTypeEnum]
}
oldValue={
oldPerformerDetails?.breast_type &&
BreastTypes[
oldPerformerDetails.breast_type as keyof typeof BreastTypeEnum
]
}
showDiff={showDiff}
/>
<ChangeRow
name="Bra Size"
newValue={`${performerDetails.band_size || ""}${
performerDetails.cup_size ?? ""
}`}
oldValue={`${oldPerformerDetails?.band_size || ""}${
oldPerformerDetails?.cup_size ?? ""
}`}
showDiff={showDiff}
/>
<ChangeRow
name="Waist Size"
newValue={performerDetails.waist_size}
oldValue={oldPerformerDetails?.waist_size}
showDiff={showDiff}
/>
<ChangeRow
name="Hip Size"
newValue={performerDetails.hip_size}
oldValue={oldPerformerDetails?.hip_size}
showDiff={showDiff}
/>
<ChangeRow
name="Nationality"
newValue={getCountryByISO(performerDetails.country)}
oldValue={getCountryByISO(oldPerformerDetails?.country)}
showDiff={showDiff}
/>
<ChangeRow
name="Ethnicity"
newValue={
performerDetails.ethnicity &&
EthnicityTypes[performerDetails.ethnicity as keyof typeof EthnicityEnum]
}
oldValue={
oldPerformerDetails?.ethnicity &&
EthnicityTypes[
oldPerformerDetails.ethnicity as keyof typeof EthnicityEnum
]
}
showDiff={showDiff}
/>
<ChangeRow
name="Career Start"
newValue={performerDetails.career_start_year}
oldValue={oldPerformerDetails?.career_start_year}
showDiff={showDiff}
/>
<ChangeRow
name="Career End"
newValue={performerDetails.career_end_year}
oldValue={oldPerformerDetails?.career_end_year}
showDiff={showDiff}
/>
<ChangeRow
name="Tattoos"
newValue={(performerDetails.added_tattoos ?? [])
.map(formatBodyModification)
.join("\n")}
oldValue={(performerDetails.removed_tattoos ?? [])
.map(formatBodyModification)
.join("\n")}
showDiff={showDiff}
/>
<ChangeRow
name="Piercings"
newValue={(performerDetails.added_piercings ?? [])
.map(formatBodyModification)
.join("\n")}
oldValue={(performerDetails.removed_piercings ?? [])
.map(formatBodyModification)
.join("\n")}
showDiff={showDiff}
/>
<URLChangeRow
newURLs={performerDetails.added_urls}
oldURLs={performerDetails.removed_urls}
showDiff={showDiff}
/>
<ImageChangeRow
newImages={performerDetails.added_images}
oldImages={performerDetails.removed_images}
showDiff={showDiff}
/>
{performerDetails.draft_id && (
<Row className="mb-2">
<Col xs={{ offset: 2 }}>
<h6>
<Icon icon={faEdit} color="green" />
<span className="ms-1">Submitted by draft</span>
</h6>
</Col>
</Row>
)}
</>
);
type ScenePerformance = {
as?: string | null;
performer: Pick<
PerformerFragment,
"name" | "id" | "gender" | "disambiguation" | "deleted"
>;
};
export interface SceneDetails {
title?: string | null;
date?: string | null;
production_date?: string | null;
duration?: number | null;
details?: string | null;
director?: string | null;
code?: string | null;
studio?: {
id: string;
name: string;
} | null;
added_performers?: ScenePerformance[] | null;
removed_performers?: ScenePerformance[] | null;
added_images?: (Image | null)[] | null;
removed_images?: (Image | null)[] | null;
added_urls?: URL[] | null;
removed_urls?: URL[] | null;
added_tags?:
| {
id: string;
name: string;
description?: string | null;
}[]
| null;
removed_tags?:
| {
id: string;
name: string;
description?: string | null;
}[]
| null;
added_fingerprints?:
| {
algorithm: FingerprintAlgorithm;
hash: string;
duration: number;
}[]
| null;
removed_fingerprints?:
| {
algorithm: FingerprintAlgorithm;
hash: string;
duration: number;
}[]
| null;
draft_id?: string | null;
}
export type OldSceneDetails = TargetOldDetails<SceneDetails>;
export const renderSceneDetails = (
sceneDetails: SceneDetails,
oldSceneDetails: OldSceneDetails | undefined,
showDiff: boolean,
) => (
<>
{sceneDetails.title && (
<ChangeRow
name="Title"
newValue={sceneDetails.title}
oldValue={oldSceneDetails?.title}
showDiff={showDiff}
/>
)}
<ChangeRow
name="Date"
newValue={sceneDetails.date}
oldValue={oldSceneDetails?.date}
showDiff={showDiff}
/>
<ChangeRow
name="Duration"
newValue={formatDuration(sceneDetails.duration)}
oldValue={formatDuration(oldSceneDetails?.duration)}
showDiff={showDiff}
/>
<ListChangeRow
name="Performers"
added={sceneDetails.added_performers}
removed={sceneDetails.removed_performers}
renderItem={renderPerformer}
getKey={(o) => o.performer.id}
showDiff={showDiff}
/>
<LinkedChangeRow
name="Studio"
newEntity={{
name: sceneDetails.studio?.name,
link: sceneDetails.studio && studioHref(sceneDetails.studio),
}}
oldEntity={{
name: oldSceneDetails?.studio?.name,
link: oldSceneDetails?.studio && studioHref(oldSceneDetails.studio),
}}
showDiff={showDiff}
/>
<URLChangeRow
newURLs={sceneDetails.added_urls}
oldURLs={sceneDetails.removed_urls}
showDiff={showDiff}
/>
<ChangeRow
name="Details"
newValue={sceneDetails.details}
oldValue={oldSceneDetails?.details}
showDiff={showDiff}
/>
<ChangeRow
name="Director"
newValue={sceneDetails.director}
oldValue={oldSceneDetails?.director}
showDiff={showDiff}
/>
<ChangeRow
name="Production Date"
newValue={sceneDetails.production_date}
oldValue={oldSceneDetails?.production_date}
showDiff={showDiff}
/>
<ChangeRow
name="Studio Code"
newValue={sceneDetails.code}
oldValue={oldSceneDetails?.code}
showDiff={showDiff}
/>
<ListChangeRow
name="Tags"
added={sceneDetails.added_tags?.slice().sort(compareByName)}
removed={sceneDetails.removed_tags?.slice().sort(compareByName)}
renderItem={renderTag}
getKey={(o) => o.id}
showDiff={showDiff}
/>
<ImageChangeRow
newImages={sceneDetails?.added_images}
oldImages={sceneDetails?.removed_images}
showDiff={showDiff}
/>
<ListChangeRow
name="Fingerprints"
added={sceneDetails.added_fingerprints}
removed={sceneDetails.removed_fingerprints}
renderItem={renderFingerprint}
getKey={(o) => `${o.hash}${o.algorithm}`}
showDiff={showDiff}
/>
{sceneDetails.draft_id && (
<Row className="mb-2">
<Col xs={{ offset: 2 }}>
<h6>
<Icon icon={faEdit} color="green" />
<span className="ms-1">Submitted by draft</span>
</h6>
</Col>
</Row>
)}
</>
);
export interface StudioDetails {
name?: string | null;
parent?: {
id: string;
name: string;
} | null;
added_images?: (Image | null)[] | null;
removed_images?: (Image | null)[] | null;
added_urls?: URL[] | null;
removed_urls?: URL[] | null;
added_aliases?: string[] | null;
removed_aliases?: string[] | null;
}
export type OldStudioDetails = TargetOldDetails<StudioDetails>;
export const renderStudioDetails = (
studioDetails: StudioDetails,
oldStudioDetails: OldStudioDetails | undefined,
showDiff: boolean,
) => (
<>
<ChangeRow
name="Name"
newValue={studioDetails.name}
oldValue={oldStudioDetails?.name}
showDiff={showDiff}
/>
<ChangeRow
name="Aliases"
newValue={studioDetails.added_aliases?.join(", ")}
oldValue={studioDetails.removed_aliases?.join(", ")}
showDiff={showDiff}
/>
<LinkedChangeRow
name="Network"
newEntity={{
name: studioDetails.parent?.name,
link: studioDetails.parent && studioHref(studioDetails.parent),
}}
oldEntity={{
name: oldStudioDetails?.parent?.name,
link: oldStudioDetails?.parent && studioHref(oldStudioDetails.parent),
}}
showDiff={showDiff}
/>
<URLChangeRow
newURLs={studioDetails.added_urls}
oldURLs={studioDetails.removed_urls}
showDiff={showDiff}
/>
<ImageChangeRow
newImages={studioDetails.added_images}
oldImages={studioDetails.removed_images}
showDiff={showDiff}
/>
</>
);
interface ModifyEditProps {
details: Details | null;
oldDetails?: OldDetails | null;
options?: Options;
}
const ModifyEdit: FC<ModifyEditProps> = ({ details, oldDetails, options }) => {
if (!details) return null;
const showDiff = !!oldDetails;
if (
isTagEdit(details) &&
(isTagEdit(oldDetails) || oldDetails === undefined)
) {
return renderTagDetails(details, oldDetails, showDiff);
}
if (
isPerformerEdit(details) &&
(isPerformerEdit(oldDetails) || oldDetails === undefined)
) {
return renderPerformerDetails(
details,
oldDetails,
showDiff,
options?.set_modify_aliases,
);
}
if (
isStudioEdit(details) &&
(isStudioEdit(oldDetails) || oldDetails === undefined)
) {
return renderStudioDetails(details, oldDetails, showDiff);
}
if (
isSceneEdit(details) &&
(isSceneEdit(oldDetails) || oldDetails === undefined)
) {
return renderSceneDetails(details, oldDetails, showDiff);
}
return null;
};
export default ModifyEdit;
================================================
FILE: frontend/src/components/editCard/VoteBar.tsx
================================================
import { type FC, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import {
VoteStatusEnum,
VoteTypeEnum,
useVote,
type EditFragment,
} from "src/graphql";
import { Icon } from "src/components/fragments";
import { useCurrentUser } from "src/hooks";
const CLASSNAME = "VoteBar";
const CLASSNAME_BUTTON = `${CLASSNAME}-button`;
const CLASSNAME_VOTED = `${CLASSNAME}-voted`;
const CLASSNAME_SAVE = `${CLASSNAME}-save`;
interface Props {
edit: EditFragment;
}
const VoteBar: FC<Props> = ({ edit }) => {
const { isVoter, isSelf } = useCurrentUser();
const userVote = (edit.votes ?? []).find((v) => v.user?.id && isSelf(v.user));
const [vote, setVote] = useState<VoteTypeEnum | null>(userVote?.vote ?? null);
const [submitVote, { loading: savingVote }] = useVote();
if (edit.status !== VoteStatusEnum.PENDING) return null;
const currentVote = (
<h6>
<span className="me-2">Current Vote:</span>
<span>{`${edit.vote_count > 0 ? "+" : ""}${
edit.vote_count === 0 ? "-" : edit.vote_count
}`}</span>
</h6>
);
// Only show vote total for edit owner and users without vote role
if (!isVoter || isSelf(edit.user)) return <div>{currentVote}</div>;
const handleSave = () => {
if (!vote) return;
submitVote({
variables: {
input: {
id: edit.id,
vote,
},
},
});
};
return (
<div className={CLASSNAME}>
<div className={CLASSNAME_SAVE}>
{currentVote}
{vote && vote !== userVote?.vote && (
<Button
variant="secondary"
onClick={handleSave}
disabled={savingVote}
>
<span className="me-2">Save</span>
<Icon icon={faCheck} color="green" />
</Button>
)}
</div>
<Form.Group
controlId={`${edit.id}-vote-yes`}
className={cx(CLASSNAME_BUTTON, {
[CLASSNAME_VOTED]: userVote?.vote === VoteTypeEnum.ACCEPT,
"bg-success": vote === VoteTypeEnum.ACCEPT,
})}
onChange={() => setVote(VoteTypeEnum.ACCEPT)}
>
<Form.Label>
<Form.Check
type="radio"
name={`${edit.id}-vote`}
defaultChecked={userVote?.vote === VoteTypeEnum.ACCEPT}
/>
<span>Yes</span>
</Form.Label>
</Form.Group>
<Form.Group
controlId={`${edit.id}-vote-no`}
className={cx(CLASSNAME_BUTTON, {
[CLASSNAME_VOTED]: userVote?.vote === VoteTypeEnum.REJECT,
"bg-danger": vote === VoteTypeEnum.REJECT,
})}
onChange={() => setVote(VoteTypeEnum.REJECT)}
>
<Form.Label>
<Form.Check
type="radio"
name={`${edit.id}-vote`}
defaultChecked={userVote?.vote === VoteTypeEnum.REJECT}
/>
<span>No</span>
</Form.Label>
</Form.Group>
<Form.Group
controlId={`${edit.id}-vote-abstain`}
className={cx(CLASSNAME_BUTTON, {
[CLASSNAME_VOTED]: userVote?.vote === VoteTypeEnum.ABSTAIN,
"bg-warning": vote === VoteTypeEnum.ABSTAIN,
})}
onChange={() => setVote(VoteTypeEnum.ABSTAIN)}
>
<Form.Label>
<Form.Check
type="radio"
name={`${edit.id}-vote`}
defaultChecked={userVote?.vote === VoteTypeEnum.ABSTAIN}
/>
<span>Abstain</span>
</Form.Label>
</Form.Group>
</div>
);
};
export default VoteBar;
================================================
FILE: frontend/src/components/editCard/Votes.tsx
================================================
import type { FC } from "react";
import { Link } from "react-router-dom";
import { sortBy } from "lodash-es";
import { VoteTypeEnum, type EditFragment } from "src/graphql";
import { userHref, formatDateTime } from "src/utils";
import { VoteTypes } from "src/constants/enums";
import { Tooltip } from "src/components/fragments";
const CLASSNAME = "EditVotes";
interface VotesProps {
edit: EditFragment;
}
const Votes: FC<VotesProps> = ({ edit }) => (
<>
<div className={CLASSNAME}>
<h5>Votes:</h5>
<div>
<b className="me-2">Vote Tally:</b>
<b>{edit.votes.filter((v) => v.vote === VoteTypeEnum.ACCEPT).length}</b>
<span className="mx-1">yes —</span>
<b>{edit.votes.filter((v) => v.vote === VoteTypeEnum.REJECT).length}</b>
<span className="ms-1">no</span>
</div>
{sortBy(edit.votes, (v) => v.date)
.filter((v) => v.vote !== VoteTypeEnum.ABSTAIN)
.map(
(v) =>
v.user && (
<div key={`${edit.id}${v.user.id}`}>
<Tooltip text={formatDateTime(v.date)}>
<Link to={userHref(v.user)}>{v.user.name}</Link>
</Tooltip>
<span className="mx-2">•</span>
{VoteTypes[v.vote]}
</div>
),
)}
</div>
</>
);
export default Votes;
================================================
FILE: frontend/src/components/editCard/index.ts
================================================
import EditCard from "./EditCard";
export default EditCard;
================================================
FILE: frontend/src/components/editCard/renderEntity.tsx
================================================
import { Link } from "react-router-dom";
import type { FingerprintAlgorithm, PerformerFragment } from "src/graphql";
import { performerHref, tagHref, createHref, formatDuration } from "src/utils";
import { GenderIcon, PerformerName, TagLink } from "src/components/fragments";
import { ROUTE_SCENES } from "src/constants";
type Appearance = {
performer: PerformerFragment;
as: string;
};
export const renderPerformer = (appearance: {
as?: string | null;
performer: Pick<
Appearance["performer"],
"name" | "id" | "gender" | "disambiguation" | "deleted"
>;
}) => (
<Link key={appearance.performer.id} to={performerHref(appearance.performer)}>
<GenderIcon gender={appearance.performer.gender} />
<PerformerName performer={appearance.performer} as={appearance.as} />
</Link>
);
export const renderTag = (tag: {
id: string;
name: string;
description?: string | null;
}) => (
<TagLink title={tag.name} link={tagHref(tag)} description={tag.description} />
);
export const renderFingerprint = (fingerprint: {
hash: string;
duration: number;
algorithm: FingerprintAlgorithm;
}) => (
<>
<Link to={`${createHref(ROUTE_SCENES)}?fingerprint=${fingerprint.hash}`}>
{fingerprint.algorithm}: {fingerprint.hash}
</Link>
<span title={`${fingerprint.duration}s`}>
{", duration: "}
{formatDuration(fingerprint.duration)}
</span>
</>
);
================================================
FILE: frontend/src/components/editCard/styles.scss
================================================
.EditComment {
background-color: $textfield-bg;
margin-top: 1rem;
.card-body a {
color: $link-color;
}
blockquote {
border-left: 3px solid $muted-gray;
padding-left: 1rem;
margin-left: 0;
font-style: italic;
color: $muted-gray;
}
}
.EditDiff {
border-radius: 0.25rem;
height: 100%;
padding: 0.25rem 0.5rem;
white-space: pre-wrap;
}
.VoteBar {
display: flex;
&-voted {
font-weight: bold;
}
&-save {
margin-left: auto;
margin-right: 20px;
}
&-button {
margin-right: 8px;
margin-top: 8px;
min-width: 34px;
text-align: center;
&:last-child {
margin-right: 0;
}
.form-label {
padding: 0.5rem;
margin-bottom: 0;
}
.form-check-input {
margin: auto;
width: 16px;
}
.form-label,
.form-check-input {
&:hover {
cursor: pointer;
}
}
}
}
.EditVotes {
margin-bottom: 20px;
text-align: right;
}
.ListChangeRow {
&-Tags {
ul {
list-style-type: none;
padding-left: 0;
li {
display: inline-block;
}
}
}
}
================================================
FILE: frontend/src/components/editImages/editImages.tsx
================================================
import { type FC, type ChangeEvent, useState } from "react";
import { Button, Col, Form, Row } from "react-bootstrap";
import { useFieldArray } from "react-hook-form";
import type { Lens } from "@hookform/lenses";
import { CombinedGraphQLErrors } from "@apollo/client";
import { faImages } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import { type ImageFragment as Image, useAddImage } from "src/graphql";
import { Image as ImageInput } from "src/components/form";
import { Icon, LoadingIndicator } from "src/components/fragments";
const CLASSNAME = "EditImages";
const CLASSNAME_IMAGES = `${CLASSNAME}-images`;
const CLASSNAME_INPUT = `${CLASSNAME}-input`;
const CLASSNAME_INPUT_CONTAINER = `${CLASSNAME_INPUT}-container`;
const CLASSNAME_DROP = `${CLASSNAME}-drop`;
const CLASSNAME_PLACEHOLDER = `${CLASSNAME}-placeholder`;
const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
const CLASSNAME_UPLOADING = `${CLASSNAME_IMAGE}-uploading`;
interface EditImagesProps {
lens: Lens<Image[]>;
file: File | undefined;
setFile: (f: File | undefined) => void;
maxImages?: number;
/** Whether to allow svg/png image input */
allowLossless?: boolean;
original?: Image[] | undefined;
}
const EditImages: FC<EditImagesProps> = ({
lens,
maxImages,
file,
setFile,
allowLossless = false,
original,
}) => {
const interop = lens.interop();
const {
fields: images,
append,
remove,
replace,
} = useFieldArray({
control: interop.control,
name: interop.name,
keyName: "key",
});
const [imageData, setImageData] = useState<string>("");
const [uploading, setUploading] = useState(false);
const [addImage] = useAddImage();
const [error, setError] = useState<string>();
const handleAddImage = () => {
setError("");
setUploading(true);
addImage({
variables: {
imageData: { file },
},
})
.then((i) => {
if (i.data?.imageCreate?.id) {
if (!images.some((image) => image.id === i.data?.imageCreate?.id)) {
append(i.data.imageCreate);
}
setFile(undefined);
setImageData("");
}
})
.catch((error: unknown) => {
if (CombinedGraphQLErrors.is(error)) setError(error.message);
})
.finally(() => {
setUploading(false);
});
};
const removeImage = () => {
setFile(undefined);
setError("");
setImageData("");
};
const onFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid && event.target.files?.[0]) {
setFile(event.target.files[0]);
const reader = new FileReader();
reader.onload = (e) =>
e.target?.result && setImageData(e.target.result as string);
reader.onerror = () => setImageData("");
reader.onabort = () => setImageData("");
reader.readAsDataURL(event.target.files[0]);
}
};
const isDisabled = maxImages !== undefined && images.length >= maxImages;
return (
<Row className={`${CLASSNAME} w-100`}>
<Col xs={7} className={CLASSNAME_IMAGES}>
{images.map((i, index) => (
<ImageInput image={i} onRemove={() => remove(index)} key={i.id} />
))}
</Col>
<Col xs={5} className={CLASSNAME_INPUT}>
<div className={CLASSNAME_INPUT_CONTAINER}>
{file ? (
<div
className={cx(CLASSNAME_IMAGE, {
[CLASSNAME_UPLOADING]: uploading,
})}
>
<img src={imageData} alt="" />
<LoadingIndicator message="Uploading image..." />
</div>
) : (
!isDisabled && (
<div className={CLASSNAME_DROP}>
<Form.Control
type="file"
onChange={onFileChange}
accept={[
".jpg",
".jpeg",
".webp",
".jfif",
...(allowLossless ? [".svg", ".png"] : []),
].join(",")}
/>
<div className={CLASSNAME_PLACEHOLDER}>
<Icon icon={faImages} />
<span>Add image</span>
</div>
</div>
)
)}
</div>
<Row className="text-end text-danger">
<div>{error}</div>
</Row>
<div className="mt-4 d-flex">
{file && (
<>
<Button
variant="danger"
onClick={() => removeImage()}
disabled={!file || uploading}
>
Remove
</Button>
<Button
onClick={() => handleAddImage()}
disabled={!file || uploading}
className="ms-2"
>
Upload
</Button>
</>
)}
<Button
variant="danger"
onClick={() => original && replace(original)}
disabled={original === undefined}
className="ms-auto mt-auto"
>
Reset Images
</Button>
</div>
</Col>
</Row>
);
};
export default EditImages;
================================================
FILE: frontend/src/components/editImages/index.ts
================================================
import EditImages from "./editImages";
export default EditImages;
================================================
FILE: frontend/src/components/editImages/styles.scss
================================================
.EditImages {
&-drop {
align-items: center;
border: 3px dashed $text-color;
border-radius: 10px;
display: flex;
height: 400px;
justify-content: center;
margin: auto;
position: relative;
width: 400px;
&:hover {
background-color: #394b59;
}
}
&-placeholder {
align-items: center;
display: flex;
flex-direction: column;
font-size: 2rem;
text-align: center;
.fa-images {
font-size: 4rem;
}
}
&-images {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
&-input-container {
display: flex;
min-height: 200px;
}
&-image {
margin: auto;
max-height: 600px;
max-width: 100%;
position: relative;
img {
max-height: 600px;
max-width: 100%;
}
.LoadingIndicator {
display: none;
position: absolute;
text-align: center;
text-shadow: 2px 2px black;
top: 40%;
}
&-uploading {
img {
opacity: 0.5;
}
.LoadingIndicator {
display: block;
opacity: 1;
}
}
}
.form-control {
cursor: pointer;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
width: 100%;
}
}
================================================
FILE: frontend/src/components/form/BodyModification.tsx
================================================
// biome-ignore-all lint/correctness/noNestedComponentDefinitions: react-select
import type { FC, ChangeEvent } from "react";
import Creatable from "react-select/creatable";
import { components } from "react-select";
import { Button, Col, Form, InputGroup, Row } from "react-bootstrap";
import { useFieldArray } from "react-hook-form";
import type { Lens } from "@hookform/lenses";
export type BodyModItem = {
location: string;
description?: string | null | undefined;
};
interface BodyModificationProps {
name: string;
lens: Lens<BodyModItem[]>;
locationPlaceholder: string;
descriptionPlaceholder: string;
formatLabel: (text: string) => string;
}
const CLASSNAME = "BodyModification";
const BodyModification: FC<BodyModificationProps> = ({
name,
locationPlaceholder,
descriptionPlaceholder,
lens,
formatLabel,
}) => {
const interop = lens.interop();
const {
fields: modifications,
append,
remove,
update,
} = useFieldArray({
control: interop.control,
name: interop.name,
keyName: "key",
});
const isNewLocationValid = (inputValue: string): boolean =>
!!inputValue &&
!modifications.find(({ location }) => inputValue === location);
const handleNewLocation = (inputValue: string) => {
append({ location: inputValue });
};
const modificationList = modifications.map((mod, index) => (
<Row key={mod.location} className="mb-1">
<InputGroup className="col">
<InputGroup.Text className="fw-bold">Location</InputGroup.Text>
<Form.Control defaultValue={mod.location} readOnly />
<Form.Control
defaultValue={mod.description ?? ""}
placeholder={descriptionPlaceholder}
onInput={(e: ChangeEvent<HTMLInputElement>) =>
update(index, {
location: mod.location,
description: e.currentTarget.value,
})
}
/>
<Button variant="danger" onClick={() => remove(index)}>
Remove
</Button>
</InputGroup>
</Row>
));
return (
<>
<Row className={CLASSNAME}>
<Col className="mb-3">
<Form.Label className="text-capitalize">{name}</Form.Label>
<Creatable
classNamePrefix="react-select"
value={null}
name={name}
placeholder={locationPlaceholder}
isValidNewOption={isNewLocationValid}
onCreateOption={handleNewLocation}
formatCreateLabel={formatLabel}
components={{
DropdownIndicator: () => null,
Menu: (data) =>
data.options.length > 0 ? <components.Menu {...data} /> : null,
}}
/>
</Col>
</Row>
{modificationList}
</>
);
};
export default BodyModification;
================================================
FILE: frontend/src/components/form/EditNote.tsx
================================================
import type { FC } from "react";
import { Form } from "react-bootstrap";
import cx from "classnames";
import type { FieldError, UseFormRegister } from "react-hook-form";
import NoteInput from "./NoteInput";
interface Props {
// biome-ignore lint/suspicious/noExplicitAny: Awkward react-hook-form type
register: UseFormRegister<any>;
error?: FieldError;
}
const EditNote: FC<Props> = ({ register, error }) => (
<div className="mb-3">
<Form.Label>Edit Note</Form.Label>
<NoteInput
className={cx({ "is-invalid": error })}
register={register}
hasError={!!error?.message}
/>
<Form.Text>
Please add any relevant sources or other supporting information for your
edit.
</Form.Text>
<Form.Control.Feedback type="invalid">
{error?.message}
</Form.Control.Feedback>
</div>
);
export default EditNote;
================================================
FILE: frontend/src/components/form/Image.tsx
================================================
import type { FC } from "react";
import { Button } from "react-bootstrap";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { Icon } from "src/components/fragments";
import Image from "src/components/image";
import type { ImageFragment } from "src/graphql";
interface ImageProps {
image: Pick<ImageFragment, "id" | "url" | "width" | "height">;
onRemove: () => void;
}
const CLASSNAME = "ImageInput";
const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
const CLASSNAME_REMOVE = `${CLASSNAME}-remove`;
const ImageInput: FC<ImageProps> = ({ image, onRemove }) => (
<div className={CLASSNAME}>
<Button
variant="danger"
className={CLASSNAME_REMOVE}
onClick={() => onRemove()}
>
<Icon icon={faXmark} />
</Button>
<Image images={image} className={CLASSNAME_IMAGE} size="full" />
</div>
);
export default ImageInput;
================================================
FILE: frontend/src/components/form/NavButtons.tsx
================================================
import type { FC } from "react";
import { Button } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
interface Props {
onNext: () => void;
disabled?: boolean;
}
export const NavButtons: FC<Props> = ({ onNext, disabled = false }) => {
const navigate = useNavigate();
return (
<div className="d-flex mt-2">
<Button
variant="danger"
className="ms-auto me-2"
onClick={() => navigate(-1)}
>
Cancel
</Button>
<Button className="me-1" onClick={onNext} disabled={disabled}>
Next
</Button>
</div>
);
};
================================================
FILE: frontend/src/components/form/NoteInput.tsx
================================================
import { type FC, type ChangeEvent, useState } from "react";
import { Form, Tabs, Tab } from "react-bootstrap";
import cx from "classnames";
import EditComment from "src/components/editCard/EditComment";
import type { UseFormRegister } from "react-hook-form";
import { useCurrentUser } from "src/hooks";
interface IProps {
onChange?: (text: string) => void;
className?: string;
register?: UseFormRegister<{ note: string }>;
hasError?: boolean;
}
const NoteInput: FC<IProps> = ({
onChange,
className,
register,
hasError = false,
}) => {
const { user } = useCurrentUser();
const [comment, setComment] = useState("");
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setComment(e.currentTarget.value);
onChange?.(e.currentTarget.value);
};
const textareaProps = register ? register("note") : { name: "note" };
const now = new Date().toISOString();
return (
<div className={cx("NoteInput", { "is-invalid": hasError })}>
<Tabs id="add-comment">
<Tab eventKey="write" title="Write" className="NoteInput-tab">
<Form.Control
as="textarea"
className={className}
onInput={handleChange}
rows={5}
{...textareaProps}
/>
</Tab>
<Tab eventKey="preview" title="Preview" unmountOnExit mountOnEnter>
<EditComment
id={`${user?.id}-${now}`}
comment={comment}
date={now}
user={user}
/>
</Tab>
</Tabs>
</div>
);
};
export default NoteInput;
================================================
FILE: frontend/src/components/form/SubmitButtons.tsx
================================================
import type { FC } from "react";
import { Button } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
interface Props {
disabled?: boolean;
}
export const SubmitButtons: FC<Props> = ({ disabled = false }) => {
const navigate = useNavigate();
return (
<div className="d-flex mt-2">
<Button
variant="danger"
className="ms-auto me-2"
onClick={() => navigate(-1)}
>
Cancel
</Button>
<Button type="submit" disabled className="d-none" aria-hidden="true" />
<Button type="submit" disabled={disabled}>
Submit Edit
</Button>
</div>
);
};
================================================
FILE: frontend/src/components/form/index.ts
================================================
export { default as BodyModification } from "./BodyModification";
export { default as Image } from "./Image";
export { default as EditNote } from "./EditNote";
export { default as NoteInput } from "./NoteInput";
export * from "./NavButtons";
export * from "./SubmitButtons";
================================================
FILE: frontend/src/components/form/styles.scss
================================================
.BodyModification {
&-remove {
z-index: 2;
background: transparent;
border: none;
color: rgba(0 0 0 / 50%);
width: 10%;
&:hover {
color: rgba(0 0 0 / 80%);
}
}
&-select {
width: 100%;
}
&-label {
width: 90%;
}
&-location {
display: inline-block;
margin-right: 5%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
width: 30%;
}
.form-control {
display: inline;
width: 65%;
}
h6 {
text-transform: capitalize;
}
}
.ImageInput {
margin: 0.5rem;
position: relative;
max-height: 200px;
&-image {
border-radius: 3px;
height: 200px;
}
&-remove {
z-index: 2;
left: 5px;
opacity: 0.6;
padding: 0 5px;
position: absolute;
top: 5px;
}
&:hover &-remove {
opacity: 1;
}
}
.NoteInput {
.tab-content {
padding-bottom: 0;
}
.EditComment {
margin-bottom: 0;
}
}
================================================
FILE: frontend/src/components/fragments/ErrorMessage.tsx
================================================
import type { FC, ReactNode } from "react";
interface IProps {
error: string | ReactNode;
}
const ErrorMessage: FC<IProps> = ({ error }) => (
<div className="row ErrorMessage">
<h2 className="ErrorMessage-content">Error: {error}</h2>
</div>
);
export default ErrorMessage;
================================================
FILE: frontend/src/components/fragments/Favorite.tsx
================================================
import type { FC, MouseEvent } from "react";
import { faStar } from "@fortawesome/free-solid-svg-icons";
import { faStar as farStar } from "@fortawesome/free-regular-svg-icons";
import { Button } from "react-bootstrap";
import cx from "classnames";
import { Icon, Tooltip } from "src/components/fragments";
import { useSetFavorite } from "src/graphql";
const CLASSNAME = "FavoriteStar";
interface Props {
entity: {
id: string;
deleted: boolean;
is_favorite: boolean;
};
entityType: "performer" | "studio";
className?: string;
interactable?: boolean;
}
export const FavoriteStar: FC<Props> = ({
className,
entity,
entityType,
interactable = false,
}) => {
const [setFavorite] = useSetFavorite(entityType, entity.id);
const handleClick = (e: MouseEvent) => {
setFavorite({
variables: {
id: entity.id,
favorite: !entity.is_favorite,
},
});
e.preventDefault();
};
if ((!interactable && !entity.is_favorite) || entity.deleted) return null;
return (
<Tooltip
text={
interactable ? `${entity.is_favorite ? "Remove" : "Add"} Favorite` : ""
}
>
<Button
disabled={!interactable}
onClick={handleClick}
className={cx(CLASSNAME, className)}
variant="link"
>
<Icon
icon={entity.is_favorite ? faStar : farStar}
color={entity.is_favorite ? "gold" : "white"}
/>
</Button>
</Tooltip>
);
};
================================================
FILE: frontend/src/components/fragments/GenderIcon.tsx
================================================
import type { FC } from "react";
import {
faVenus,
faTransgender,
faMars,
faVenusMars,
} from "@fortawesome/free-solid-svg-icons";
import Icon from "./Icon";
import type { GenderEnum } from "src/graphql";
import { GenderTypes } from "src/constants";
interface IconProps {
gender?: GenderEnum | null;
}
const GenderIcon: FC<IconProps> = ({ gender }) => {
if (gender) {
const icon =
gender.toLowerCase() === "male"
? faMars
: gender.toLowerCase() === "female"
? faVenus
: faTransgender;
return <Icon icon={icon} title={GenderTypes[gender]} />;
}
return <Icon icon={faVenusMars} />;
};
export default GenderIcon;
================================================
FILE: frontend/src/components/fragments/Help.tsx
================================================
import type { FC } from "react";
import { Button, OverlayTrigger, Popover } from "react-bootstrap";
import { Icon } from "src/components/fragments";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
interface Props {
message: string;
}
const Help: FC<Props> = ({ message }) => {
const renderContent = () => (
<Popover id="help">
<Popover.Body>{message}</Popover.Body>
</Popover>
);
return (
<OverlayTrigger
overlay={renderContent()}
placement="bottom"
trigger="hover"
>
<Button variant="link" className="minimal">
<Icon icon={faQuestionCircle} />
</Button>
</OverlayTrigger>
);
};
export default Help;
================================================
FILE: frontend/src/components/fragments/Icon.tsx
================================================
import type { FC } from "react";
import cx from "classnames";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
interface Props {
icon: IconDefinition;
className?: string;
color?: string;
title?: string;
variant?: "danger" | "success" | "info" | "warning";
}
const Icon: FC<Props> = ({ icon, className, color, title, variant }) => (
<FontAwesomeIcon
title={title}
icon={icon}
className={cx("fa-icon", className, { [`text-${variant}`]: variant })}
color={color}
/>
);
export default Icon;
================================================
FILE: frontend/src/components/fragments/LoadingIndicator.tsx
================================================
import { type FC, useEffect, useState } from "react";
import { Spinner } from "react-bootstrap";
import cx from "classnames";
interface LoadingProps {
message?: string;
delay?: number;
}
const CLASSNAME = "LoadingIndicator";
const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
const CLASSNAME_DELAYED = `${CLASSNAME}-delayed`;
const LoadingIndicator: FC<LoadingProps> = ({ message, delay = 100 }) => {
const [delayed, setDelayed] = useState(delay > 0);
useEffect(() => {
if (!delayed || delay === 0) return;
const timeout = setTimeout(() => setDelayed(false), delay);
return () => clearTimeout(timeout);
}, [delayed, delay]);
return (
<div className={cx(CLASSNAME, { [CLASSNAME_DELAYED]: delayed })}>
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
<h4 className={CLASSNAME_MESSAGE}>{message ?? "Loading..."}</h4>
</div>
);
};
export default LoadingIndicator;
================================================
FILE: frontend/src/components/fragments/PerformerName.tsx
================================================
import type { FC } from "react";
import type { PerformerFragment } from "src/graphql";
interface PerformerNameProps {
performer: Pick<PerformerFragment, "name" | "disambiguation" | "deleted">;
as?: string | null;
}
const PerformerName: FC<PerformerNameProps> = ({ performer, as }) => {
if (!as)
return (
<>
{performer.deleted ? (
<del>{performer.name}</del>
) : (
<span>{performer.name}</span>
)}
{performer.disambiguation && (
<small className="ms-1 text-small text-muted">
({performer.disambiguation})
</small>
)}
</>
);
return (
<>
<span>{as}</span>
<small className="ms-1 text-small text-muted">
({performer.name})
{performer.disambiguation && (
<small className="ms-1 text-small text-muted">
({performer.disambiguation})
</small>
)}
</small>
</>
);
};
export default PerformerName;
================================================
FILE: frontend/src/components/fragments/SearchHint.tsx
================================================
import type React from "react";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
import { Icon, Tooltip } from "src/components/fragments";
export const SearchHint: React.FC = () => (
<Tooltip text='Add " to the end to include all words, or paste in a Stash ID'>
<div className="SearchHint">
<Icon icon={faCircleQuestion} color="black" />
</div>
</Tooltip>
);
================================================
FILE: frontend/src/components/fragments/SearchInput.tsx
================================================
import { components } from "react-select";
import { extractIdFromUrl } from "src/utils";
// Shared Input component for react-select that extracts IDs from pasted stash-box URLs
export const SearchInput: typeof components.Input = (props) => (
<components.Input
{...props}
onPaste={(e) => {
const pasted = e.clipboardData.getData("text/plain");
const extracted = extractIdFromUrl(pasted);
if (extracted !== pasted) {
e.preventDefault();
props.selectProps.onInputChange(extracted, {
action: "input-change",
prevInputValue: String(props.value ?? ""),
});
}
}}
/>
);
================================================
FILE: frontend/src/components/fragments/SiteLink.tsx
================================================
import type { FC } from "react";
import { Link } from "react-router-dom";
import cx from "classnames";
import { siteHref } from "src/utils/route";
const CLASSNAME = "SiteLink";
const CLASSNAME_ICON = `${CLASSNAME}-icon`;
const CLASSNAME_NAME = `${CLASSNAME}-name`;
const CLASSNAME_NO_MARGIN = `${CLASSNAME}-no-margin`;
interface Props {
site: {
id: string;
name: string;
icon: string;
} | null;
hideName?: boolean;
noMargin?: boolean;
}
const SiteLink: FC<Props> = ({ site, hideName = false, noMargin = false }) =>
site && (
<Link to={siteHref(site)} className={CLASSNAME}>
<img className={CLASSNAME_ICON} src={site.icon} alt="" />
{!hideName && (
<span
className={cx(CLASSNAME_NAME, { [CLASSNAME_NO_MARGIN]: noMargin })}
>
{site.name}
</span>
)}
</Link>
);
export default SiteLink;
================================================
FILE: frontend/src/components/fragments/TagLink.tsx
================================================
import type { FC } from "react";
import { Badge, Button } from "react-bootstrap";
import { Link } from "react-router-dom";
import { Icon } from "src/components/fragments";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
interface IProps {
title: string;
link?: string;
description?: string | null;
className?: string;
onRemove?: () => void;
disabled?: boolean;
}
const TagLink: FC<IProps> = ({
title,
link,
description,
className,
onRemove,
disabled = false,
}) => (
<Badge className={cx("tag-item", className)} bg="none">
<abbr title={description || undefined}>
{link && !disabled ? <Link to={link}>{title}</Link> : title}
</abbr>
{onRemove && (
<Button onClick={onRemove}>
<Icon icon={faXmark} />
</Button>
)}
</Badge>
);
export default TagLink;
================================================
FILE: frontend/src/components/fragments/Thumbnail.tsx
================================================
import type { FC } from "react";
import cx from "classnames";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import Icon from "./Icon";
interface Props {
image?: string;
size?: 600 | 300;
alt?: string | null;
className?: string;
orientation?: "portrait" | "landscape";
}
const doubleSize = {
300: 600,
600: 1280,
};
export const Thumbnail: FC<Props> = ({
image,
size,
alt,
className,
orientation = "landscape",
}) =>
image ? (
<img
alt={alt ?? ""}
className={className}
src={image + (size ? `?size=${size}` : "")}
srcSet={
size ? `${image}?size=${doubleSize[size]} ${doubleSize[size]}w` : ""
}
/>
) : (
<div
className={cx(className, "Thumbnail-empty")}
style={{ aspectRatio: orientation === "landscape" ? "16/9" : "2/3" }}
>
<Icon icon={faXmark} />
</div>
);
================================================
FILE: frontend/src/components/fragments/Tooltip.tsx
================================================
import type { FC, ReactElement } from "react";
import {
OverlayTrigger,
Tooltip as BSTooltip,
type PopoverProps,
} from "react-bootstrap";
interface Props {
text: string | ReactElement;
placement?: PopoverProps["placement"];
children: ReactElement;
delay?: number;
}
const Tooltip: FC<Props> = ({
children,
text,
delay = 200,
placement = "bottom-end",
}) => (
<OverlayTrigger
delay={{ show: delay, hide: 0 }}
overlay={
<BSTooltip className="Tooltip" id="tooltip">
{text}
</BSTooltip>
}
show={text ? undefined : false}
placement={placement}
trigger={["hover", "focus"]}
>
{children}
</OverlayTrigger>
);
export default Tooltip;
================================================
FILE: frontend/src/components/fragments/index.ts
================================================
export { default as GenderIcon } from "./GenderIcon";
export { default as LoadingIndicator } from "./LoadingIndicator";
export { default as Icon } from "./Icon";
export { default as TagLink } from "./TagLink";
export { default as SiteLink } from "./SiteLink";
export { default as PerformerName } from "./PerformerName";
export { default as ErrorMessage } from "./ErrorMessage";
export { default as Help } from "./Help";
export { default as Tooltip } from "./Tooltip";
export { FavoriteStar } from "./Favorite";
export { Thumbnail } from "./Thumbnail";
export { SearchHint } from "./SearchHint";
export { SearchInput } from "./SearchInput";
================================================
FILE: frontend/src/components/fragments/styles.scss
================================================
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.LoadingIndicator {
align-items: center;
display: flex;
flex-direction: column;
height: 70vh;
justify-content: center;
width: 100%;
opacity: 1;
transition: opacity 1s linear;
&-delayed {
opacity: 0;
}
&-message {
margin-top: 1rem;
}
.spinner-border {
height: 3rem;
width: 3rem;
}
}
.tag-item {
background-color: #bfccd6;
color: #182026;
font-size: 12px;
font-weight: 400;
line-height: 16px;
margin: 5px;
padding: 2px 6px;
a:hover {
color: unset;
text-decoration: none;
}
&:hover {
cursor: pointer;
}
.btn {
background: none;
border: none;
color: #182026;
margin: 0;
margin-left: 0.25rem;
padding: 0 0.5rem;
&:hover {
background-color: #ffbdad;
color: #de350b;
}
.fa-xmark {
width: 0.5rem;
}
}
}
.ErrorMessage {
align-items: center;
height: 20rem;
justify-content: center;
&-content {
display: inline-block;
}
}
.SiteLink {
align-items: baseline;
display: inline-flex;
&-icon {
align-self: center;
border-radius: 3px;
height: 2ex;
width: 2ex;
margin-right: 5px;
}
&-name {
font-size: 2ex;
font-weight: bold;
&::after {
content: ":";
margin-right: 0.5rem;
}
}
&-no-margin::after {
display: none;
}
}
.FavoriteStar {
padding: 0;
margin-top: -4px;
&:disabled {
opacity: 1;
}
&:focus {
box-shadow: none;
}
}
.SearchHint {
margin-left: 10px;
cursor: help;
}
.Tooltip {
position: absolute !important;
}
.Thumbnail {
&-empty {
display: flex;
width: 100%;
height: 100%;
color: var(--bs-gray-400);
background-color: #394b59;
align-items: center;
justify-content: center;
border-radius: 4px;
.fa-icon {
height: 30px;
max-height: 30%;
}
}
}
================================================
FILE: frontend/src/components/image/Image.tsx
================================================
import { type FC, useState } from "react";
import cx from "classnames";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { sortImageURLs } from "src/utils";
import { LoadingIndicator, Icon } from "src/components/fragments";
const CLASSNAME = "Image";
type Image = {
url: string;
width: number;
height: number;
};
type ImageSize = 1280 | 600 | 300 | "full";
interface ImageProps {
image?: Image;
emptyMessage?: string;
size?: ImageSize;
alt?: string;
}
const ImageComponent: FC<ImageProps> = ({
image,
emptyMessage = "No image",
size,
alt,
}) => {
const [imageState, setImageState] = useState<"loading" | "error" | "done">(
"loading",
);
if (!image?.url)
return (
<div className={`${CLASSNAME}-missing`}>
<Icon icon={faXmark} color="var(--bs-gray-400)" />
<div>{emptyMessage}</div>
</div>
);
const sizeQuery = size ? `?size=${size}` : "";
return (
<>
{imageState === "loading" && (
<LoadingIndicator message="Loading image..." delay={200} />
)}
{imageState === "error" && (
<div className="Image-error">
<Icon icon={faXmark} color="red" />
<div>Failed to load image</div>
</div>
)}
<img
alt={alt ?? ""}
src={`${image.url}${sizeQuery}`}
className={`${CLASSNAME}-image`}
onLoad={() => setImageState("done")}
onError={() => setImageState("error")}
/>
</>
);
};
interface ContainerProps {
images: Image[] | Image | undefined;
orientation?: "landscape" | "portrait";
emptyMessage?: string;
size?: ImageSize;
alt?: string;
classN
gitextract_6obqz5rx/ ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── discussion---request-for-commentary--rfc-.md │ │ └── feature_request.md │ └── workflows/ │ ├── build.yml │ ├── frontend-lint.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── CLAUDE.md ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ └── stash-box/ │ ├── init.go │ └── main.go ├── docker/ │ ├── build/ │ │ └── x86_64/ │ │ ├── Dockerfile │ │ ├── db/ │ │ │ └── initdb.sh │ │ └── docker-compose.yml │ ├── ci/ │ │ └── x86_64/ │ │ ├── Dockerfile │ │ └── docker_push.sh │ └── production/ │ ├── docker-compose.yml │ └── postgres/ │ └── Dockerfile ├── frontend/ │ ├── .gitattributes │ ├── .gitignore │ ├── .nvmrc │ ├── README.md │ ├── biome.json │ ├── codegen.yml │ ├── embed.go │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.scss │ │ ├── App.tsx │ │ ├── Login.tsx │ │ ├── Main.tsx │ │ ├── components/ │ │ │ ├── amendableEditCard/ │ │ │ │ ├── AmendableChangeRow.tsx │ │ │ │ ├── AmendableImageChangeRow.tsx │ │ │ │ ├── AmendableLinkedChangeRow.tsx │ │ │ │ ├── AmendableListChangeRow.tsx │ │ │ │ ├── AmendableModifyEdit.tsx │ │ │ │ ├── AmendableURLChangeRow.tsx │ │ │ │ ├── AmendmentContext.tsx │ │ │ │ └── index.ts │ │ │ ├── changeRow/ │ │ │ │ ├── ChangeRow.tsx │ │ │ │ └── index.ts │ │ │ ├── checkboxSelect/ │ │ │ │ ├── CheckboxSelect.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── deleteButton/ │ │ │ │ ├── DeleteButton.tsx │ │ │ │ └── index.ts │ │ │ ├── editCard/ │ │ │ │ ├── AddComment.tsx │ │ │ │ ├── EditCard.tsx │ │ │ │ ├── EditComment.tsx │ │ │ │ ├── EditExpiration.tsx │ │ │ │ ├── EditHeader.tsx │ │ │ │ ├── EditStatus.tsx │ │ │ │ ├── ModifyEdit.tsx │ │ │ │ ├── VoteBar.tsx │ │ │ │ ├── Votes.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── renderEntity.tsx │ │ │ │ └── styles.scss │ │ │ ├── editImages/ │ │ │ │ ├── editImages.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── form/ │ │ │ │ ├── BodyModification.tsx │ │ │ │ ├── EditNote.tsx │ │ │ │ ├── Image.tsx │ │ │ │ ├── NavButtons.tsx │ │ │ │ ├── NoteInput.tsx │ │ │ │ ├── SubmitButtons.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── fragments/ │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ ├── Favorite.tsx │ │ │ │ ├── GenderIcon.tsx │ │ │ │ ├── Help.tsx │ │ │ │ ├── Icon.tsx │ │ │ │ ├── LoadingIndicator.tsx │ │ │ │ ├── PerformerName.tsx │ │ │ │ ├── SearchHint.tsx │ │ │ │ ├── SearchInput.tsx │ │ │ │ ├── SiteLink.tsx │ │ │ │ ├── TagLink.tsx │ │ │ │ ├── Thumbnail.tsx │ │ │ │ ├── Tooltip.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── image/ │ │ │ │ ├── Image.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── imageCarousel/ │ │ │ │ ├── ImageCarousel.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── imageChangeRow/ │ │ │ │ ├── ImageChangeRow.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── linkedChangeRow/ │ │ │ │ ├── LinkedChangeRow.tsx │ │ │ │ └── index.ts │ │ │ ├── list/ │ │ │ │ ├── EditList.tsx │ │ │ │ ├── List.tsx │ │ │ │ ├── SceneList.tsx │ │ │ │ ├── TagList.tsx │ │ │ │ ├── URLList.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── listChangeRow/ │ │ │ │ ├── ListChangeRow.tsx │ │ │ │ └── index.ts │ │ │ ├── modal/ │ │ │ │ ├── Modal.tsx │ │ │ │ └── index.ts │ │ │ ├── multiSelect/ │ │ │ │ ├── MultiSelect.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── pagination/ │ │ │ │ ├── Pagination.tsx │ │ │ │ └── index.ts │ │ │ ├── performerCard/ │ │ │ │ ├── PerformerCard.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── performerSelect/ │ │ │ │ ├── PerformerSelect.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── sceneCard/ │ │ │ │ ├── SceneCard.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── searchField/ │ │ │ │ ├── SearchField.tsx │ │ │ │ ├── handleResult.ts │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── studioSelect/ │ │ │ │ ├── StudioSelect.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── tagFilter/ │ │ │ │ ├── TagFilter.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── tagSelect/ │ │ │ │ ├── TagSelect.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── title/ │ │ │ │ ├── Title.tsx │ │ │ │ └── index.ts │ │ │ ├── urlChangeRow/ │ │ │ │ ├── URLChangeRow.tsx │ │ │ │ └── index.ts │ │ │ └── urlInput/ │ │ │ ├── index.ts │ │ │ ├── styles.scss │ │ │ └── urlInput.tsx │ │ ├── constants/ │ │ │ ├── enums.ts │ │ │ ├── index.ts │ │ │ └── route.ts │ │ ├── context.tsx │ │ ├── graphql/ │ │ │ ├── fragments/ │ │ │ │ ├── CommentFragment.gql │ │ │ │ ├── EditFragment.gql │ │ │ │ ├── FingerprintFragment.gql │ │ │ │ ├── ImageFragment.gql │ │ │ │ ├── PerformerFragment.gql │ │ │ │ ├── QuerySceneFragment.gql │ │ │ │ ├── SceneFragment.gql │ │ │ │ ├── ScenePerformerFragment.gql │ │ │ │ ├── SearchPerformerFragment.gql │ │ │ │ ├── StudioFragment.gql │ │ │ │ ├── TagFragment.gql │ │ │ │ └── URLFragment.gql │ │ │ ├── index.ts │ │ │ ├── mutations/ │ │ │ │ ├── ActivateNewUser.gql │ │ │ │ ├── AddImage.gql │ │ │ │ ├── AddScene.gql │ │ │ │ ├── AddSite.gql │ │ │ │ ├── AddStudio.gql │ │ │ │ ├── AddTagCategory.gql │ │ │ │ ├── AddUser.gql │ │ │ │ ├── AmendEdit.gql │ │ │ │ ├── ApplyEdit.gql │ │ │ │ ├── CancelEdit.gql │ │ │ │ ├── ChangePassword.gql │ │ │ │ ├── ConfirmChangeEmail.gql │ │ │ │ ├── DeleteDraft.gql │ │ │ │ ├── DeleteEdit.gql │ │ │ │ ├── DeleteFingerprintSubmissions.gql │ │ │ │ ├── DeleteScene.gql │ │ │ │ ├── DeleteSite.gql │ │ │ │ ├── DeleteStudio.gql │ │ │ │ ├── DeleteTagCategory.gql │ │ │ │ ├── DeleteUser.gql │ │ │ │ ├── EditComment.gql │ │ │ │ ├── FavoritePerformer.gql │ │ │ │ ├── FavoriteStudio.gql │ │ │ │ ├── GenerateInviteCode.gql │ │ │ │ ├── GrantInvite.gql │ │ │ │ ├── MarkNotificationRead.gql │ │ │ │ ├── MarkNotificationsRead.gql │ │ │ │ ├── MoveFingerprintSubmissions.gql │ │ │ │ ├── NewUser.gql │ │ │ │ ├── PerformerEdit.gql │ │ │ │ ├── PerformerEditUpdate.gql │ │ │ │ ├── RegenerateAPIKey.gql │ │ │ │ ├── RequestChangeEmail.gql │ │ │ │ ├── RescindInviteCode.gql │ │ │ │ ├── ResetPassword.gql │ │ │ │ ├── RevokeInvite.gql │ │ │ │ ├── SceneEdit.gql │ │ │ │ ├── SceneEditUpdate.gql │ │ │ │ ├── StudioEdit.gql │ │ │ │ ├── StudioEditUpdate.gql │ │ │ │ ├── TagEdit.gql │ │ │ │ ├── TagEditUpdate.gql │ │ │ │ ├── UnmatchFingerprint.gql │ │ │ │ ├── UpdateNotificationSubscriptions.gql │ │ │ │ ├── UpdateScene.gql │ │ │ │ ├── UpdateSite.gql │ │ │ │ ├── UpdateStudio.gql │ │ │ │ ├── UpdateTagCategory.gql │ │ │ │ ├── UpdateUser.gql │ │ │ │ ├── ValidateChangeEmail.gql │ │ │ │ ├── Vote.gql │ │ │ │ └── index.ts │ │ │ ├── queries/ │ │ │ │ ├── Categories.gql │ │ │ │ ├── Category.gql │ │ │ │ ├── Config.gql │ │ │ │ ├── Draft.gql │ │ │ │ ├── Drafts.gql │ │ │ │ ├── Edit.gql │ │ │ │ ├── EditUpdate.gql │ │ │ │ ├── Edits.gql │ │ │ │ ├── FullPerformer.gql │ │ │ │ ├── Me.gql │ │ │ │ ├── ModAudits.gql │ │ │ │ ├── PendingEditsCount.gql │ │ │ │ ├── Performer.gql │ │ │ │ ├── Performers.gql │ │ │ │ ├── PublicUser.gql │ │ │ │ ├── QueryExistingPerformer.gql │ │ │ │ ├── QueryExistingScene.gql │ │ │ │ ├── QueryNotifications.gql │ │ │ │ ├── Scene.gql │ │ │ │ ├── ScenePairings.gql │ │ │ │ ├── Scenes.gql │ │ │ │ ├── ScenesWithFingerprints.gql │ │ │ │ ├── ScenesWithoutCount.gql │ │ │ │ ├── SearchAll.gql │ │ │ │ ├── SearchPerformers.gql │ │ │ │ ├── SearchScenes.gql │ │ │ │ ├── SearchTags.gql │ │ │ │ ├── Site.gql │ │ │ │ ├── Sites.gql │ │ │ │ ├── Studio.gql │ │ │ │ ├── StudioPerformers.gql │ │ │ │ ├── Studios.gql │ │ │ │ ├── SubStudios.gql │ │ │ │ ├── Tag.gql │ │ │ │ ├── Tags.gql │ │ │ │ ├── UnreadNotificationCount.gql │ │ │ │ ├── User.gql │ │ │ │ ├── Users.gql │ │ │ │ ├── Version.gql │ │ │ │ └── index.ts │ │ │ ├── scalars.d.ts │ │ │ └── types.ts │ │ ├── hooks/ │ │ │ ├── index.ts │ │ │ ├── toast.scss │ │ │ ├── useAuth.tsx │ │ │ ├── useBeforeUnload.ts │ │ │ ├── useCurrentUser.tsx │ │ │ ├── useEditFilter.tsx │ │ │ ├── usePagination.ts │ │ │ ├── useQueryParams.ts │ │ │ └── useToast.tsx │ │ ├── index.tsx │ │ ├── modules.d.ts │ │ ├── pages/ │ │ │ ├── activateUser/ │ │ │ │ ├── ActivateUser.tsx │ │ │ │ └── index.ts │ │ │ ├── audits/ │ │ │ │ ├── AmendmentAuditDetails.tsx │ │ │ │ ├── AuditRow.tsx │ │ │ │ ├── Audits.tsx │ │ │ │ ├── DeleteAuditDetails.tsx │ │ │ │ └── index.tsx │ │ │ ├── categories/ │ │ │ │ ├── Categories.tsx │ │ │ │ ├── Category.tsx │ │ │ │ ├── CategoryAdd.tsx │ │ │ │ ├── CategoryEdit.tsx │ │ │ │ ├── categoryForm/ │ │ │ │ │ ├── CategoryForm.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.tsx │ │ │ ├── drafts/ │ │ │ │ ├── Draft.tsx │ │ │ │ ├── Drafts.tsx │ │ │ │ ├── PerformerDraft.tsx │ │ │ │ ├── SceneDraft.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── parse.ts │ │ │ ├── edits/ │ │ │ │ ├── Edit.tsx │ │ │ │ ├── EditAmend.tsx │ │ │ │ ├── EditAmendForm.tsx │ │ │ │ ├── EditUpdate.tsx │ │ │ │ ├── Edits.tsx │ │ │ │ ├── components/ │ │ │ │ │ ├── DeleteEditModal.tsx │ │ │ │ │ └── UpdateCount.tsx │ │ │ │ └── index.tsx │ │ │ ├── forgotPassword/ │ │ │ │ ├── ForgotPassword.tsx │ │ │ │ └── index.ts │ │ │ ├── home/ │ │ │ │ ├── Home.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.scss │ │ │ ├── index.tsx │ │ │ ├── notifications/ │ │ │ │ ├── CommentNotification.tsx │ │ │ │ ├── EditNotification.tsx │ │ │ │ ├── Notification.tsx │ │ │ │ ├── Notifications.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── sceneNotification.tsx │ │ │ │ ├── styles.scss │ │ │ │ └── types.ts │ │ │ ├── performers/ │ │ │ │ ├── Performer.tsx │ │ │ │ ├── PerformerAdd.tsx │ │ │ │ ├── PerformerDelete.tsx │ │ │ │ ├── PerformerEdit.tsx │ │ │ │ ├── PerformerEditUpdate.tsx │ │ │ │ ├── PerformerMerge.tsx │ │ │ │ ├── Performers.tsx │ │ │ │ ├── components/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── performerInfo.tsx │ │ │ │ │ └── scenePairings.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── performerForm/ │ │ │ │ │ ├── ExistingPerformerAlert.tsx │ │ │ │ │ ├── PerformerForm.tsx │ │ │ │ │ ├── diff.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ └── styles.scss │ │ │ ├── registerUser/ │ │ │ │ ├── Register.tsx │ │ │ │ └── index.ts │ │ │ ├── resetPassword/ │ │ │ │ ├── ResetPassword.tsx │ │ │ │ └── index.ts │ │ │ ├── scenes/ │ │ │ │ ├── Scene.tsx │ │ │ │ ├── SceneAdd.tsx │ │ │ │ ├── SceneDelete.tsx │ │ │ │ ├── SceneEdit.tsx │ │ │ │ ├── SceneEditUpdate.tsx │ │ │ │ ├── Scenes.tsx │ │ │ │ ├── components/ │ │ │ │ │ └── fingerprints/ │ │ │ │ │ ├── DeleteFingerprintsModal.tsx │ │ │ │ │ ├── FingerprintTable.tsx │ │ │ │ │ ├── FingerprintTableHeader.tsx │ │ │ │ │ ├── FingerprintTableRow.tsx │ │ │ │ │ ├── MoveFingerprintsModal.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── useFingerprintOperations.ts │ │ │ │ │ ├── useFingerprintSelection.ts │ │ │ │ │ └── useFingerprintSort.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── sceneForm/ │ │ │ │ │ ├── ExistingSceneAlert.tsx │ │ │ │ │ ├── SceneForm.tsx │ │ │ │ │ ├── diff.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── types.ts │ │ │ │ └── styles.scss │ │ │ ├── search/ │ │ │ │ ├── GenderFacet.tsx │ │ │ │ ├── PerformerCard.tsx │ │ │ │ ├── SceneCard.tsx │ │ │ │ ├── SearchAll.tsx │ │ │ │ ├── SearchLayout.tsx │ │ │ │ ├── SearchPerformersTab.tsx │ │ │ │ ├── SearchScenesTab.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── search.scss │ │ │ ├── sites/ │ │ │ │ ├── Site.tsx │ │ │ │ ├── SiteAdd.tsx │ │ │ │ ├── SiteEdit.tsx │ │ │ │ ├── Sites.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── siteForm/ │ │ │ │ ├── SiteForm.tsx │ │ │ │ └── index.ts │ │ │ ├── studios/ │ │ │ │ ├── Studio.tsx │ │ │ │ ├── StudioAdd.tsx │ │ │ │ ├── StudioDelete.tsx │ │ │ │ ├── StudioEdit.tsx │ │ │ │ ├── StudioEditUpdate.tsx │ │ │ │ ├── Studios.tsx │ │ │ │ ├── components/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── studioPerformers.tsx │ │ │ │ │ ├── subStudioList.tsx │ │ │ │ │ └── subStudioPreview.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── studioForm/ │ │ │ │ │ ├── StudioForm.tsx │ │ │ │ │ ├── diff.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ └── styles.scss │ │ │ ├── tags/ │ │ │ │ ├── Tag.tsx │ │ │ │ ├── TagAdd.tsx │ │ │ │ ├── TagDelete.tsx │ │ │ │ ├── TagEdit.tsx │ │ │ │ ├── TagEditUpdate.tsx │ │ │ │ ├── TagMerge.tsx │ │ │ │ ├── Tags.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── tagForm/ │ │ │ │ ├── TagForm.tsx │ │ │ │ ├── diff.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── types.ts │ │ │ ├── users/ │ │ │ │ ├── GenerateInviteKeyModal.tsx │ │ │ │ ├── User.tsx │ │ │ │ ├── UserAdd.tsx │ │ │ │ ├── UserConfirmChangeEmail.tsx │ │ │ │ ├── UserEdit.tsx │ │ │ │ ├── UserEditForm.tsx │ │ │ │ ├── UserEdits.tsx │ │ │ │ ├── UserFingerprints.tsx │ │ │ │ ├── UserFingerprintsList/ │ │ │ │ │ ├── UserFingerprint.tsx │ │ │ │ │ ├── UserFingerprintsList.tsx │ │ │ │ │ ├── UserSceneLine.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── UserForm.tsx │ │ │ │ ├── UserNotificationPreferences.tsx │ │ │ │ ├── UserPassword.tsx │ │ │ │ ├── UserPasswordForm.tsx │ │ │ │ ├── UserValidateChangeEmail.tsx │ │ │ │ ├── Users.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.scss │ │ │ └── version/ │ │ │ ├── Version.tsx │ │ │ └── index.ts │ │ ├── styles/ │ │ │ └── theme.scss │ │ └── utils/ │ │ ├── country.ts │ │ ├── createClient.ts │ │ ├── data.ts │ │ ├── date.ts │ │ ├── diff.ts │ │ ├── edit.ts │ │ ├── enum.ts │ │ ├── general.ts │ │ ├── index.ts │ │ ├── intl.ts │ │ ├── markdown.tsx │ │ ├── route.ts │ │ ├── transforms.ts │ │ ├── url.ts │ │ └── user.ts │ ├── tsconfig.json │ └── vite.config.mjs ├── go.mod ├── go.sum ├── gqlgen.yml ├── graphql/ │ └── schema/ │ ├── schema.graphql │ └── types/ │ ├── config.graphql │ ├── draft.graphql │ ├── edit.graphql │ ├── filter.graphql │ ├── image.graphql │ ├── misc.graphql │ ├── mod_audit.graphql │ ├── notifications.graphql │ ├── performer.graphql │ ├── scene.graphql │ ├── site.graphql │ ├── studio.graphql │ ├── tag.graphql │ ├── user.graphql │ └── version.graphql ├── internal/ │ ├── api/ │ │ ├── context_keys.go │ │ ├── directives.go │ │ ├── draft_integration_test.go │ │ ├── edit_amend_integration_test.go │ │ ├── edit_delete_integration_test.go │ │ ├── edit_integration_test.go │ │ ├── field_test.go │ │ ├── fingerprint_filter.go │ │ ├── graphql_client_test.go │ │ ├── integration_test.go │ │ ├── loaders.go │ │ ├── notification_integration_test.go │ │ ├── performer_edit_integration_test.go │ │ ├── performer_integration_test.go │ │ ├── performer_resolver_integration_test.go │ │ ├── resolver.go │ │ ├── resolver_model_draft.go │ │ ├── resolver_model_edit.go │ │ ├── resolver_model_edit_comment.go │ │ ├── resolver_model_edit_vote.go │ │ ├── resolver_model_image.go │ │ ├── resolver_model_notification.go │ │ ├── resolver_model_performer.go │ │ ├── resolver_model_performer_draft.go │ │ ├── resolver_model_performer_edit.go │ │ ├── resolver_model_scene.go │ │ ├── resolver_model_scene_draft.go │ │ ├── resolver_model_scene_edit.go │ │ ├── resolver_model_site.go │ │ ├── resolver_model_studio.go │ │ ├── resolver_model_studio_edit.go │ │ ├── resolver_model_tag.go │ │ ├── resolver_model_tag_category.go │ │ ├── resolver_model_tag_edit.go │ │ ├── resolver_model_url.go │ │ ├── resolver_model_user.go │ │ ├── resolver_mutation_draft.go │ │ ├── resolver_mutation_edit.go │ │ ├── resolver_mutation_image.go │ │ ├── resolver_mutation_notifications.go │ │ ├── resolver_mutation_performer.go │ │ ├── resolver_mutation_scene.go │ │ ├── resolver_mutation_site.go │ │ ├── resolver_mutation_studio.go │ │ ├── resolver_mutation_tag.go │ │ ├── resolver_mutation_tag_category.go │ │ ├── resolver_mutation_user.go │ │ ├── resolver_query_draft.go │ │ ├── resolver_query_edit.go │ │ ├── resolver_query_mod_audit.go │ │ ├── resolver_query_notifications.go │ │ ├── resolver_query_performer.go │ │ ├── resolver_query_scene.go │ │ ├── resolver_query_site.go │ │ ├── resolver_query_studio.go │ │ ├── resolver_query_tag.go │ │ ├── resolver_query_tag_category.go │ │ ├── resolver_query_user.go │ │ ├── routes_image.go │ │ ├── routes_root.go │ │ ├── scene_edit_integration_test.go │ │ ├── scene_integration_test.go │ │ ├── search_integration_test.go │ │ ├── server.go │ │ ├── session.go │ │ ├── site_integration_test.go │ │ ├── studio_edit_integration_test.go │ │ ├── studio_integration_test.go │ │ ├── tag_category_integration_test.go │ │ ├── tag_edit_integration_test.go │ │ ├── tag_integration_test.go │ │ ├── user_integration_test.go │ │ └── utils.go │ ├── auth/ │ │ └── authorization.go │ ├── autocert/ │ │ └── autocert.go │ ├── config/ │ │ ├── config.go │ │ └── paths.go │ ├── converter/ │ │ ├── converter.go │ │ └── gen/ │ │ ├── extensions.go │ │ ├── generated.go │ │ └── interfaces.go │ ├── cron/ │ │ └── cron.go │ ├── database/ │ │ ├── database.go │ │ ├── migrations/ │ │ │ └── postgres/ │ │ │ ├── 01_initial.down.sql │ │ │ ├── 01_initial.up.sql │ │ │ ├── 02_create_search.down.sql │ │ │ ├── 02_create_search.up.sql │ │ │ ├── 03_misc.up.sql │ │ │ ├── 04_image_tables.up.sql │ │ │ ├── 05_edits.up.sql │ │ │ ├── 06_deletion_and_redirects.up.sql │ │ │ ├── 07_optimization_indexes.up.sql │ │ │ ├── 08_user_invite.up.sql │ │ │ ├── 09_image_data.up.sql │ │ │ ├── 10_tag_categories.up.sql │ │ │ ├── 11_image_constraints.up.sql │ │ │ ├── 12_fix_performer_trigger.up.sql │ │ │ ├── 13_sort_indexes.up.sql │ │ │ ├── 14_phash_distance_search.up.sql │ │ │ ├── 15_scene_fingerprint_submissions.up.sql │ │ │ ├── 16_fix_scene_update_trigger.up.sql │ │ │ ├── 17_edit_votes.up.sql │ │ │ ├── 18_fingerprint_user.up.sql │ │ │ ├── 19_scene_created_index.up.sql │ │ │ ├── 20_edit_constraints.up.sql │ │ │ ├── 21_site_urls.up.sql │ │ │ ├── 22_performer_search_indexes.up.sql │ │ │ ├── 23_favorites.up.sql │ │ │ ├── 24_drafts.up.sql │ │ │ ├── 25_scene_codes.up.sql │ │ │ ├── 26_scene_partial_date.down.sql │ │ │ ├── 26_scene_partial_date.up.sql │ │ │ ├── 27_edit_closed_at.up.sql │ │ │ ├── 28_studio_favorite_index.up.sql │ │ │ ├── 29_scene_edit_fingerprint_index.up.sql │ │ │ ├── 30_edit_bot.up.sql │ │ │ ├── 31_scenes_deleted_idx.up.sql │ │ │ ├── 32_edit_indexes.up.sql │ │ │ ├── 33_invite_key_uses.up.sql │ │ │ ├── 34_fingerprints.up.sql │ │ │ ├── 35_websearch.up.sql │ │ │ ├── 36_drop_unique_invite.up.sql │ │ │ ├── 37_tokens.up.sql │ │ │ ├── 38_scenes_studio_id_index.up.sql │ │ │ ├── 39_edits_updates.up.sql │ │ │ ├── 40_fingerprint_vote.up.sql │ │ │ ├── 41_notifications.up.sql │ │ │ ├── 42_date_columns.up.sql │ │ │ ├── 43_studio_aliases.up.sql │ │ │ ├── 44_performer_death_date.up.sql │ │ │ ├── 45_scene_production_date.up.sql │ │ │ ├── 46_update_default_notifications.up.sql │ │ │ ├── 47_favorite_unique.up.sql │ │ │ ├── 48_fingerprinted_scene_edit_notification.up.sql │ │ │ ├── 49_entity_search_lower_idx.up.sql │ │ │ ├── 50_rename_url_siteid.up.sql │ │ │ ├── 51_scene_deleted_sort_indexes.up.sql │ │ │ ├── 52_fingerprint_hash_bigint.up.sql │ │ │ ├── 53_varchar_to_text.up.sql │ │ │ ├── 54_delete_soft_deleted_aliases.up.sql │ │ │ ├── 55_fix_edit_data_dates.up.sql │ │ │ ├── 56_paradedb_search.up.sql │ │ │ └── 57_mod_audit.up.sql │ │ └── testutil/ │ │ └── testutil.go │ ├── dataloader/ │ │ ├── bodymodificationsloader_gen.go │ │ ├── boolsloader_gen.go │ │ ├── editcommentloader_gen.go │ │ ├── editloader_gen.go │ │ ├── fingerprintsloader_gen.go │ │ ├── imageloader_gen.go │ │ ├── loaders.go │ │ ├── performerloader_gen.go │ │ ├── sceneappearancesloader_gen.go │ │ ├── sceneloader_gen.go │ │ ├── siteloader_gen.go │ │ ├── stringsloader_gen.go │ │ ├── studioloader_gen.go │ │ ├── submittedfingerprintsloader_gen.go │ │ ├── tagcategoryloader_gen.go │ │ ├── tagloader_gen.go │ │ ├── urlloader_gen.go │ │ └── uuidsloader_gen.go │ ├── email/ │ │ ├── manager.go │ │ ├── templates/ │ │ │ ├── email.html │ │ │ └── email.txt │ │ └── user.go │ ├── image/ │ │ ├── cache/ │ │ │ └── cache.go │ │ ├── resize_unix.go │ │ ├── resize_windows.go │ │ └── sort.go │ ├── models/ │ │ ├── assign/ │ │ │ └── assign.go │ │ ├── extension_criterion_input.go │ │ ├── extension_edit_details.go │ │ ├── extension_edit_details_test.go │ │ ├── extension_role_enum.go │ │ ├── generate.go │ │ ├── generated_exec.go │ │ ├── generated_models.go │ │ ├── model_draft.go │ │ ├── model_edit.go │ │ ├── model_image.go │ │ ├── model_invite_key.go │ │ ├── model_mod_audit.go │ │ ├── model_notification.go │ │ ├── model_performer.go │ │ ├── model_scene.go │ │ ├── model_site.go │ │ ├── model_studio.go │ │ ├── model_tag.go │ │ ├── model_tag_category.go │ │ ├── model_user.go │ │ ├── model_user_tokens.go │ │ ├── scalars.go │ │ ├── translate.go │ │ ├── url.go │ │ └── validator/ │ │ └── validator.go │ ├── queries/ │ │ ├── copyfrom.go │ │ ├── db.go │ │ ├── draft.sql.go │ │ ├── edit.sql.go │ │ ├── fingerprint.sql.go │ │ ├── helpers.go │ │ ├── image.sql.go │ │ ├── invite_key.sql.go │ │ ├── mod_audit.sql.go │ │ ├── models.go │ │ ├── notification.sql.go │ │ ├── performer.sql.go │ │ ├── querier.go │ │ ├── scene.sql.go │ │ ├── site.sql.go │ │ ├── sql/ │ │ │ ├── draft.sql │ │ │ ├── edit.sql │ │ │ ├── fingerprint.sql │ │ │ ├── image.sql │ │ │ ├── invite_key.sql │ │ │ ├── mod_audit.sql │ │ │ ├── notification.sql │ │ │ ├── performer.sql │ │ │ ├── scene.sql │ │ │ ├── site.sql │ │ │ ├── studio.sql │ │ │ ├── tag.sql │ │ │ ├── tag_category.sql │ │ │ ├── user.sql │ │ │ └── user_token.sql │ │ ├── studio.sql.go │ │ ├── tag.sql.go │ │ ├── tag_category.sql.go │ │ ├── types.go │ │ ├── user.sql.go │ │ └── user_token.sql.go │ ├── service/ │ │ ├── draft/ │ │ │ └── service.go │ │ ├── edit/ │ │ │ ├── edit.go │ │ │ ├── modbot.go │ │ │ ├── performer.go │ │ │ ├── query.go │ │ │ ├── scene.go │ │ │ ├── service.go │ │ │ ├── studio.go │ │ │ ├── tag.go │ │ │ └── validate.go │ │ ├── errutil/ │ │ │ └── errors.go │ │ ├── factory.go │ │ ├── image/ │ │ │ ├── service.go │ │ │ └── utils.go │ │ ├── interface.go │ │ ├── invite/ │ │ │ └── service.go │ │ ├── mod_audit/ │ │ │ └── mod_audit.go │ │ ├── notification/ │ │ │ └── service.go │ │ ├── performer/ │ │ │ ├── joins.go │ │ │ ├── query.go │ │ │ └── service.go │ │ ├── query/ │ │ │ ├── criterion.go │ │ │ └── helpers.go │ │ ├── scene/ │ │ │ ├── query.go │ │ │ └── service.go │ │ ├── site/ │ │ │ ├── query.go │ │ │ └── service.go │ │ ├── studio/ │ │ │ ├── query.go │ │ │ └── service.go │ │ ├── tag/ │ │ │ ├── query.go │ │ │ └── service.go │ │ ├── user/ │ │ │ ├── activation.go │ │ │ ├── apikey.go │ │ │ ├── invite.go │ │ │ ├── joins.go │ │ │ ├── password.go │ │ │ ├── query.go │ │ │ ├── service.go │ │ │ ├── token.go │ │ │ ├── user.go │ │ │ └── validate.go │ │ └── usertoken/ │ │ └── service.go │ └── storage/ │ ├── favicon.go │ ├── file.go │ ├── image_backend.go │ └── s3.go ├── pkg/ │ ├── logger/ │ │ ├── logger.go │ │ ├── otel.go │ │ └── progress_formatter.go │ └── utils/ │ ├── arguments.go │ ├── crypto.go │ ├── date.go │ ├── enum.go │ ├── file.go │ ├── json.go │ ├── password_blacklist.go │ ├── slice_compare.go │ └── slice_compare_test.go ├── scripts/ │ └── getDate.go └── sqlc.yaml
Showing preview only (678K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (6216 symbols across 426 files)
FILE: cmd/stash-box/init.go
function initConfig (line 16) | func initConfig(configFilePath *string) {
function parseConfigFilePath (line 80) | func parseConfigFilePath(configFilePath string) (string, string) {
function initEnvs (line 88) | func initEnvs() {
FILE: cmd/stash-box/main.go
function main (line 19) | func main() {
function blockForever (line 54) | func blockForever() {
FILE: frontend/src/Login.tsx
type LoginFormData (line 19) | type LoginFormData = yup.InferType<typeof schema>;
FILE: frontend/src/Main.tsx
type Props (line 34) | interface Props {
FILE: frontend/src/components/amendableEditCard/AmendableChangeRow.tsx
type AmendableChangeRowProps (line 9) | interface AmendableChangeRowProps {
FILE: frontend/src/components/amendableEditCard/AmendableImageChangeRow.tsx
type Image (line 10) | type Image = {
constant CLASSNAME (line 17) | const CLASSNAME = "ImageChangeRow";
constant CLASSNAME_IMAGE (line 18) | const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
type AmendableImageChangeRowProps (line 20) | interface AmendableImageChangeRowProps {
FILE: frontend/src/components/amendableEditCard/AmendableLinkedChangeRow.tsx
type Change (line 10) | interface Change {
type AmendableLinkedChangeRowProps (line 15) | interface AmendableLinkedChangeRowProps {
function getValue (line 33) | function getValue(value: Change | null | undefined) {
FILE: frontend/src/components/amendableEditCard/AmendableListChangeRow.tsx
type AmendableListChangeRowProps (line 9) | interface AmendableListChangeRowProps<T> {
constant CLASSNAME (line 19) | const CLASSNAME = "ListChangeRow";
FILE: frontend/src/components/amendableEditCard/AmendableModifyEdit.tsx
type Details (line 61) | type Details = EditFragment["details"];
type OldDetails (line 62) | type OldDetails = EditFragment["old_details"];
type Options (line 63) | type Options = EditFragment["options"];
type AmendableModifyEditProps (line 576) | interface AmendableModifyEditProps {
FILE: frontend/src/components/amendableEditCard/AmendableURLChangeRow.tsx
constant CLASSNAME (line 10) | const CLASSNAME = "URLChangeRow";
type AmendableURLChangeRowProps (line 12) | interface AmendableURLChangeRowProps {
FILE: frontend/src/components/amendableEditCard/AmendmentContext.tsx
type AmendmentState (line 10) | interface AmendmentState {
type AmendmentContextValue (line 16) | interface AmendmentContextValue {
type AmendmentProviderProps (line 39) | interface AmendmentProviderProps {
FILE: frontend/src/components/changeRow/ChangeRow.tsx
type ChangeRowProps (line 5) | interface ChangeRowProps {
FILE: frontend/src/components/checkboxSelect/CheckboxSelect.tsx
type MultiSelectProps (line 7) | interface MultiSelectProps {
type IOptionType (line 15) | interface IOptionType {
FILE: frontend/src/components/deleteButton/DeleteButton.tsx
type DeleteButtonProps (line 7) | interface DeleteButtonProps {
FILE: frontend/src/components/editCard/AddComment.tsx
type IProps (line 10) | interface IProps {
FILE: frontend/src/components/editCard/EditCard.tsx
constant CLASSNAME (line 20) | const CLASSNAME = "EditCard";
type Props (line 22) | interface Props {
FILE: frontend/src/components/editCard/EditComment.tsx
constant CLASSNAME (line 7) | const CLASSNAME = "EditComment";
type Props (line 9) | interface Props {
FILE: frontend/src/components/editCard/EditExpiration.tsx
type Props (line 13) | interface Props {
FILE: frontend/src/components/editCard/EditHeader.tsx
type Target (line 18) | type Target = NonNullable<EditFragment["target"]>;
type EditHeaderProps (line 49) | interface EditHeaderProps {
FILE: frontend/src/components/editCard/EditStatus.tsx
type Props (line 9) | interface Props {
FILE: frontend/src/components/editCard/ModifyEdit.tsx
type Details (line 42) | type Details = EditFragment["details"];
type OldDetails (line 43) | type OldDetails = EditFragment["old_details"];
type Options (line 44) | type Options = EditFragment["options"];
type Image (line 46) | type Image = {
type StartingWith (line 54) | type StartingWith<T, K extends string> = T extends `${K}${infer _}` ? T ...
type TargetOldDetails (line 55) | type TargetOldDetails<T> = Omit<
type TagDetails (line 60) | interface TagDetails {
type OldTagDetails (line 68) | type OldTagDetails = TargetOldDetails<TagDetails>;
type BodyMod (line 109) | type BodyMod = {
type PerformerDetails (line 114) | interface PerformerDetails {
type OldPerformerDetails (line 145) | type OldPerformerDetails = TargetOldDetails<PerformerDetails>;
type ScenePerformance (line 356) | type ScenePerformance = {
type SceneDetails (line 364) | interface SceneDetails {
type OldSceneDetails (line 413) | type OldSceneDetails = TargetOldDetails<SceneDetails>;
type StudioDetails (line 524) | interface StudioDetails {
type OldStudioDetails (line 538) | type OldStudioDetails = TargetOldDetails<StudioDetails>;
type ModifyEditProps (line 583) | interface ModifyEditProps {
FILE: frontend/src/components/editCard/VoteBar.tsx
constant CLASSNAME (line 15) | const CLASSNAME = "VoteBar";
constant CLASSNAME_BUTTON (line 16) | const CLASSNAME_BUTTON = `${CLASSNAME}-button`;
constant CLASSNAME_VOTED (line 17) | const CLASSNAME_VOTED = `${CLASSNAME}-voted`;
constant CLASSNAME_SAVE (line 18) | const CLASSNAME_SAVE = `${CLASSNAME}-save`;
type Props (line 20) | interface Props {
FILE: frontend/src/components/editCard/Votes.tsx
constant CLASSNAME (line 10) | const CLASSNAME = "EditVotes";
type VotesProps (line 12) | interface VotesProps {
FILE: frontend/src/components/editCard/renderEntity.tsx
type Appearance (line 9) | type Appearance = {
FILE: frontend/src/components/editImages/editImages.tsx
constant CLASSNAME (line 13) | const CLASSNAME = "EditImages";
constant CLASSNAME_IMAGES (line 14) | const CLASSNAME_IMAGES = `${CLASSNAME}-images`;
constant CLASSNAME_INPUT (line 15) | const CLASSNAME_INPUT = `${CLASSNAME}-input`;
constant CLASSNAME_INPUT_CONTAINER (line 16) | const CLASSNAME_INPUT_CONTAINER = `${CLASSNAME_INPUT}-container`;
constant CLASSNAME_DROP (line 17) | const CLASSNAME_DROP = `${CLASSNAME}-drop`;
constant CLASSNAME_PLACEHOLDER (line 18) | const CLASSNAME_PLACEHOLDER = `${CLASSNAME}-placeholder`;
constant CLASSNAME_IMAGE (line 19) | const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
constant CLASSNAME_UPLOADING (line 20) | const CLASSNAME_UPLOADING = `${CLASSNAME_IMAGE}-uploading`;
type EditImagesProps (line 22) | interface EditImagesProps {
FILE: frontend/src/components/form/BodyModification.tsx
type BodyModItem (line 9) | type BodyModItem = {
type BodyModificationProps (line 14) | interface BodyModificationProps {
constant CLASSNAME (line 22) | const CLASSNAME = "BodyModification";
FILE: frontend/src/components/form/EditNote.tsx
type Props (line 8) | interface Props {
FILE: frontend/src/components/form/Image.tsx
type ImageProps (line 9) | interface ImageProps {
constant CLASSNAME (line 14) | const CLASSNAME = "ImageInput";
constant CLASSNAME_IMAGE (line 15) | const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
constant CLASSNAME_REMOVE (line 16) | const CLASSNAME_REMOVE = `${CLASSNAME}-remove`;
FILE: frontend/src/components/form/NavButtons.tsx
type Props (line 5) | interface Props {
FILE: frontend/src/components/form/NoteInput.tsx
type IProps (line 9) | interface IProps {
FILE: frontend/src/components/form/SubmitButtons.tsx
type Props (line 5) | interface Props {
FILE: frontend/src/components/fragments/ErrorMessage.tsx
type IProps (line 3) | interface IProps {
FILE: frontend/src/components/fragments/Favorite.tsx
constant CLASSNAME (line 10) | const CLASSNAME = "FavoriteStar";
type Props (line 12) | interface Props {
FILE: frontend/src/components/fragments/GenderIcon.tsx
type IconProps (line 12) | interface IconProps {
FILE: frontend/src/components/fragments/Help.tsx
type Props (line 6) | interface Props {
FILE: frontend/src/components/fragments/Icon.tsx
type Props (line 6) | interface Props {
FILE: frontend/src/components/fragments/LoadingIndicator.tsx
type LoadingProps (line 5) | interface LoadingProps {
constant CLASSNAME (line 10) | const CLASSNAME = "LoadingIndicator";
constant CLASSNAME_MESSAGE (line 11) | const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
constant CLASSNAME_DELAYED (line 12) | const CLASSNAME_DELAYED = `${CLASSNAME}-delayed`;
FILE: frontend/src/components/fragments/PerformerName.tsx
type PerformerNameProps (line 4) | interface PerformerNameProps {
FILE: frontend/src/components/fragments/SiteLink.tsx
constant CLASSNAME (line 6) | const CLASSNAME = "SiteLink";
constant CLASSNAME_ICON (line 7) | const CLASSNAME_ICON = `${CLASSNAME}-icon`;
constant CLASSNAME_NAME (line 8) | const CLASSNAME_NAME = `${CLASSNAME}-name`;
constant CLASSNAME_NO_MARGIN (line 9) | const CLASSNAME_NO_MARGIN = `${CLASSNAME}-no-margin`;
type Props (line 11) | interface Props {
FILE: frontend/src/components/fragments/TagLink.tsx
type IProps (line 8) | interface IProps {
FILE: frontend/src/components/fragments/Thumbnail.tsx
type Props (line 6) | interface Props {
FILE: frontend/src/components/fragments/Tooltip.tsx
type Props (line 8) | interface Props {
FILE: frontend/src/components/image/Image.tsx
constant CLASSNAME (line 7) | const CLASSNAME = "Image";
type Image (line 9) | type Image = {
type ImageSize (line 15) | type ImageSize = 1280 | 600 | 300 | "full";
type ImageProps (line 17) | interface ImageProps {
type ContainerProps (line 66) | interface ContainerProps {
FILE: frontend/src/components/imageCarousel/ImageCarousel.tsx
type ImageCarouselProps (line 13) | interface ImageCarouselProps {
FILE: frontend/src/components/imageChangeRow/ImageChangeRow.tsx
type Image (line 5) | type Image = {
constant CLASSNAME (line 12) | const CLASSNAME = "ImageChangeRow";
constant CLASSNAME_IMAGE (line 13) | const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
type ImageChangeRowProps (line 15) | interface ImageChangeRowProps {
FILE: frontend/src/components/linkedChangeRow/LinkedChangeRow.tsx
type Change (line 6) | interface Change {
type LinkedChangeRowProps (line 11) | interface LinkedChangeRowProps {
function getValue (line 24) | function getValue(value: Change | null | undefined) {
FILE: frontend/src/components/list/EditList.tsx
type EditsProps (line 17) | interface EditsProps {
constant PER_PAGE (line 34) | const PER_PAGE = 20;
FILE: frontend/src/components/list/List.tsx
constant PER_PAGE (line 5) | const PER_PAGE = 20;
type Props (line 7) | interface Props {
FILE: frontend/src/components/list/SceneList.tsx
constant PER_PAGE (line 24) | const PER_PAGE = 20;
type Props (line 26) | interface Props {
FILE: frontend/src/components/list/TagList.tsx
constant PER_PAGE (line 18) | const PER_PAGE = 40;
type TagListProps (line 20) | interface TagListProps {
FILE: frontend/src/components/list/URLList.tsx
type URLListProps (line 4) | interface URLListProps {
FILE: frontend/src/components/listChangeRow/ListChangeRow.tsx
type ListChangeRowProps (line 5) | interface ListChangeRowProps<T> {
constant CLASSNAME (line 14) | const CLASSNAME = "ListChangeRow";
FILE: frontend/src/components/modal/Modal.tsx
type ModalProps (line 4) | interface ModalProps {
type MessageProps (line 10) | interface MessageProps {
type ElementProps (line 14) | interface ElementProps {
FILE: frontend/src/components/multiSelect/MultiSelect.tsx
type MultiSelectProps (line 6) | interface MultiSelectProps {
type IOptionType (line 12) | interface IOptionType {
FILE: frontend/src/components/pagination/Pagination.tsx
type PaginationProps (line 4) | interface PaginationProps {
FILE: frontend/src/components/performerCard/PerformerCard.tsx
type PerformerType (line 16) | type PerformerType = Pick<
type PerformerCardProps (line 21) | interface PerformerCardProps {
constant CLASSNAME (line 26) | const CLASSNAME = "PerformerCard";
constant CLASSNAME_IMAGE (line 27) | const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
constant CLASSNAME_STAR (line 28) | const CLASSNAME_STAR = `${CLASSNAME}-star`;
FILE: frontend/src/components/performerSelect/PerformerSelect.tsx
type Performer (line 9) | type Performer = NonNullable<
type PerformerSelectProps (line 13) | interface PerformerSelectProps {
constant CLASSNAME (line 20) | const CLASSNAME = "PerformerSelect";
constant CLASSNAME_LIST (line 21) | const CLASSNAME_LIST = `${CLASSNAME}-list`;
constant CLASSNAME_CONTAINER (line 22) | const CLASSNAME_CONTAINER = `${CLASSNAME}-container`;
FILE: frontend/src/components/sceneCard/SceneCard.tsx
type Performance (line 16) | type Performance = Pick<
constant CLASSNAME (line 23) | const CLASSNAME = "SceneCard";
constant CLASSNAME_IMAGE (line 24) | const CLASSNAME_IMAGE = `${CLASSNAME}-image`;
constant CLASSNAME_BODY (line 25) | const CLASSNAME_BODY = `${CLASSNAME}-body`;
FILE: frontend/src/components/searchField/SearchField.tsx
type SearchType (line 33) | enum SearchType {
type SearchFieldProps (line 38) | interface SearchFieldProps {
FILE: frontend/src/components/searchField/handleResult.ts
type SceneAllResult (line 4) | type SceneAllResult = NonNullable<
type PerformerAllResult (line 7) | type PerformerAllResult = NonNullable<
type PerformerOnlyResult (line 10) | type PerformerOnlyResult = NonNullable<
type PerformerResult (line 14) | type PerformerResult = PerformerAllResult | PerformerOnlyResult;
type SceneResult (line 15) | type SceneResult = SceneAllResult;
type SearchGroup (line 17) | interface SearchGroup {
type SearchResult (line 22) | interface SearchResult {
type PerformerSearchResult (line 29) | interface PerformerSearchResult extends SearchResult {
function formatPerformerLabel (line 38) | function formatPerformerLabel(performer: PerformerResult): string {
function formatPerformerSublabel (line 42) | function formatPerformerSublabel(
function getStudioSceneCount (line 65) | function getStudioSceneCount(performer: PerformerOnlyResult): number {
function formatSceneLabel (line 72) | function formatSceneLabel(scene: SceneResult): string {
function formatSceneSublabel (line 76) | function formatSceneSublabel(scene: SceneResult): string {
function handleSearchAllResult (line 86) | function handleSearchAllResult(
function handlePerformerSearchResult (line 117) | function handlePerformerSearchResult(
function groupPerformersByStudio (line 137) | function groupPerformersByStudio(
function createPerformerGroups (line 158) | function createPerformerGroups(performers: SearchResult[]): SearchGroup[] {
function createSceneGroups (line 163) | function createSceneGroups(scenes: SearchResult[]): SearchGroup[] {
function handleResult (line 168) | function handleResult(
FILE: frontend/src/components/studioSelect/StudioSelect.tsx
type Studio (line 21) | type Studio = NonNullable<StudioQuery["findStudio"]>;
type StudioParent (line 22) | type StudioParent = { id: string; name: string } | null;
type StudioSlim (line 23) | type StudioSlim = Pick<Studio, "id" | "name"> & { parent?: StudioParent };
type IOptionType (line 25) | interface IOptionType {
type StudioSelectProps (line 32) | interface StudioSelectProps {
constant CLASSNAME (line 48) | const CLASSNAME = "StudioSelect";
constant CLASSNAME_SELECT (line 49) | const CLASSNAME_SELECT = `${CLASSNAME}-select`;
FILE: frontend/src/components/tagFilter/TagFilter.tsx
type Tag (line 15) | type Tag = NonNullable<SearchTagsQuery["query"][number]>;
type TagFilterProps (line 17) | interface TagFilterProps {
type SearchResult (line 25) | interface SearchResult {
constant CLASSNAME (line 31) | const CLASSNAME = "TagFilter";
constant CLASSNAME_SELECT (line 32) | const CLASSNAME_SELECT = `${CLASSNAME}-select`;
FILE: frontend/src/components/tagSelect/TagSelect.tsx
type Tag (line 14) | type Tag = NonNullable<SearchTagsQuery["query"][number]>;
type TagSlim (line 16) | type TagSlim = {
type TagSelectProps (line 23) | interface TagSelectProps {
type SearchResult (line 32) | interface SearchResult {
constant CLASSNAME (line 38) | const CLASSNAME = "TagSelect";
constant CLASSNAME_LIST (line 39) | const CLASSNAME_LIST = `${CLASSNAME}-list`;
constant CLASSNAME_SELECT (line 40) | const CLASSNAME_SELECT = `${CLASSNAME}-select`;
constant CLASSNAME_CONTAINER (line 41) | const CLASSNAME_CONTAINER = `${CLASSNAME}-container`;
FILE: frontend/src/components/title/Title.tsx
constant INSTANCE_TITLE (line 5) | const INSTANCE_TITLE =
type Props (line 8) | interface Props {
FILE: frontend/src/components/urlChangeRow/URLChangeRow.tsx
constant CLASSNAME (line 5) | const CLASSNAME = "URLChangeRow";
type URL (line 7) | interface URL {
type URLChangeRowProps (line 36) | interface URLChangeRowProps {
FILE: frontend/src/components/urlInput/urlInput.tsx
type Site (line 12) | type Site = NonNullable<SiteQuery["findSite"]>;
constant CLASSNAME (line 14) | const CLASSNAME = "URLInput";
type URLItem (line 16) | type URLItem = {
type ErrorsType (line 25) | type ErrorsType = Merge<
type URLInputProps (line 30) | interface URLInputProps {
FILE: frontend/src/constants/enums.ts
type EnumDictionary (line 16) | type EnumDictionary<T extends string | symbol | number, U> = {
FILE: frontend/src/constants/route.ts
constant ROUTE_HOME (line 1) | const ROUTE_HOME = "/";
constant ROUTE_LOGIN (line 2) | const ROUTE_LOGIN = "/login";
constant ROUTE_LOGOUT (line 3) | const ROUTE_LOGOUT = "/logout";
constant ROUTE_USERS (line 4) | const ROUTE_USERS = "/users";
constant ROUTE_USER_ADD (line 5) | const ROUTE_USER_ADD = "/users/add";
constant ROUTE_USER (line 6) | const ROUTE_USER = "/users/:name";
constant ROUTE_USER_EDIT (line 7) | const ROUTE_USER_EDIT = "/users/:name/edit";
constant ROUTE_USER_PASSWORD (line 8) | const ROUTE_USER_PASSWORD = "/users/change-password";
constant ROUTE_USER_EDITS (line 9) | const ROUTE_USER_EDITS = "/users/:name/edits";
constant ROUTE_USER_MY_FINGERPRINTS (line 10) | const ROUTE_USER_MY_FINGERPRINTS = "/users/fingerprints";
constant ROUTE_PERFORMER (line 11) | const ROUTE_PERFORMER = "/performers/:id";
constant ROUTE_PERFORMER_ADD (line 12) | const ROUTE_PERFORMER_ADD = "/performers/add";
constant ROUTE_PERFORMER_EDIT (line 13) | const ROUTE_PERFORMER_EDIT = "/performers/:id/edit";
constant ROUTE_PERFORMER_MERGE (line 14) | const ROUTE_PERFORMER_MERGE = "/performers/:id/merge";
constant ROUTE_PERFORMER_DELETE (line 15) | const ROUTE_PERFORMER_DELETE = "/performers/:id/delete";
constant ROUTE_PERFORMERS (line 16) | const ROUTE_PERFORMERS = "/performers";
constant ROUTE_SCENE (line 17) | const ROUTE_SCENE = "/scenes/:id";
constant ROUTE_SCENE_ADD (line 18) | const ROUTE_SCENE_ADD = "/scenes/add";
constant ROUTE_SCENE_EDIT (line 19) | const ROUTE_SCENE_EDIT = "/scenes/:id/edit";
constant ROUTE_SCENE_DELETE (line 20) | const ROUTE_SCENE_DELETE = "/scenes/:id/delete";
constant ROUTE_SCENES (line 21) | const ROUTE_SCENES = "/scenes";
constant ROUTE_STUDIO (line 22) | const ROUTE_STUDIO = "/studios/:id";
constant ROUTE_STUDIO_ADD (line 23) | const ROUTE_STUDIO_ADD = "/studios/add";
constant ROUTE_STUDIO_EDIT (line 24) | const ROUTE_STUDIO_EDIT = "/studios/:id/edit";
constant ROUTE_STUDIO_DELETE (line 25) | const ROUTE_STUDIO_DELETE = "/studios/:id/delete";
constant ROUTE_STUDIOS (line 26) | const ROUTE_STUDIOS = "/studios";
constant ROUTE_TAG (line 27) | const ROUTE_TAG = "/tags/:id";
constant ROUTE_TAG_ADD (line 28) | const ROUTE_TAG_ADD = "/tags/add";
constant ROUTE_TAG_MERGE (line 29) | const ROUTE_TAG_MERGE = "/tags/:id/merge";
constant ROUTE_TAG_EDIT (line 30) | const ROUTE_TAG_EDIT = "/tags/:id/edit";
constant ROUTE_TAG_DELETE (line 31) | const ROUTE_TAG_DELETE = "/tags/:id/delete";
constant ROUTE_TAGS (line 32) | const ROUTE_TAGS = "/tags";
constant ROUTE_CATEGORY (line 33) | const ROUTE_CATEGORY = "/categories/:id";
constant ROUTE_CATEGORY_ADD (line 34) | const ROUTE_CATEGORY_ADD = "/categories/add";
constant ROUTE_CATEGORY_EDIT (line 35) | const ROUTE_CATEGORY_EDIT = "/categories/:id/edit";
constant ROUTE_CATEGORIES (line 36) | const ROUTE_CATEGORIES = "/categories";
constant ROUTE_EDITS (line 37) | const ROUTE_EDITS = "/edits";
constant ROUTE_EDIT (line 38) | const ROUTE_EDIT = "/edits/:id";
constant ROUTE_EDIT_UPDATE (line 39) | const ROUTE_EDIT_UPDATE = "/edits/:id/update";
constant ROUTE_EDIT_AMEND (line 40) | const ROUTE_EDIT_AMEND = "/edits/:id/amend";
constant ROUTE_REGISTER (line 41) | const ROUTE_REGISTER = "/register";
constant ROUTE_ACTIVATE (line 42) | const ROUTE_ACTIVATE = "/activate";
constant ROUTE_FORGOT_PASSWORD (line 43) | const ROUTE_FORGOT_PASSWORD = "/forgot-password";
constant ROUTE_RESET_PASSWORD (line 44) | const ROUTE_RESET_PASSWORD = "/reset-password";
constant ROUTE_CONFIRM_EMAIL (line 45) | const ROUTE_CONFIRM_EMAIL = "/users/confirm-email";
constant ROUTE_CHANGE_EMAIL (line 46) | const ROUTE_CHANGE_EMAIL = "/users/change-email";
constant ROUTE_SEARCH (line 47) | const ROUTE_SEARCH = "/search";
constant ROUTE_VERSION (line 48) | const ROUTE_VERSION = "/version";
constant ROUTE_SITE (line 49) | const ROUTE_SITE = "/sites/:id";
constant ROUTE_SITE_ADD (line 50) | const ROUTE_SITE_ADD = "/sites/add";
constant ROUTE_SITE_EDIT (line 51) | const ROUTE_SITE_EDIT = "/sites/:id/edit";
constant ROUTE_SITES (line 52) | const ROUTE_SITES = "/sites";
constant ROUTE_DRAFT (line 53) | const ROUTE_DRAFT = "/drafts/:id";
constant ROUTE_DRAFTS (line 54) | const ROUTE_DRAFTS = "/drafts";
constant ROUTE_NOTIFICATIONS (line 55) | const ROUTE_NOTIFICATIONS = "/notifications";
constant ROUTE_NOTIFICATION_SUBSCRIPTIONS (line 56) | const ROUTE_NOTIFICATION_SUBSCRIPTIONS = "/users/:name/notifications";
constant ROUTE_AUDITS (line 57) | const ROUTE_AUDITS = "/audits";
FILE: frontend/src/context.tsx
type User (line 5) | interface User {
type ContextType (line 11) | type ContextType = {
FILE: frontend/src/graphql/mutations/index.ts
method update (line 438) | update(cache, { data }, { variables }) {
method update (line 455) | update(cache, { data }, { variables }) {
method update (line 484) | update(cache, { data }, { variables }) {
method update (line 526) | update(cache, { data }) {
method update (line 541) | update(cache, { data }) {
method update (line 554) | update(cache, { data }) {
FILE: frontend/src/graphql/scalars.d.ts
type GQLDate (line 1) | type GQLDate = string;
type GQLTime (line 2) | type GQLTime = string;
type GQLUpload (line 3) | type GQLUpload = File;
FILE: frontend/src/graphql/types.ts
type Maybe (line 2) | type Maybe<T> = T | null;
type InputMaybe (line 3) | type InputMaybe<T> = Maybe<T>;
type Exact (line 4) | type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K...
type MakeOptional (line 5) | type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?:...
type MakeMaybe (line 6) | type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: May...
type MakeEmpty (line 7) | type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> ...
type Incremental (line 8) | type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' ...
type Scalars (line 10) | type Scalars = {
type ActivateNewUserInput (line 23) | type ActivateNewUserInput = {
type AmendEditInput (line 29) | type AmendEditInput = {
type AmendItemRemoval (line 40) | type AmendItemRemoval = {
type ApplyEditInput (line 47) | type ApplyEditInput = {
type BodyModification (line 51) | type BodyModification = {
type BodyModificationCriterionInput (line 57) | type BodyModificationCriterionInput = {
type BodyModificationInput (line 63) | type BodyModificationInput = {
type BreastTypeCriterionInput (line 68) | type BreastTypeCriterionInput = {
type BreastTypeEnum (line 73) | enum BreastTypeEnum {
type CancelEditInput (line 79) | type CancelEditInput = {
type CommentCommentedEdit (line 83) | type CommentCommentedEdit = {
type CommentOwnEdit (line 88) | type CommentOwnEdit = {
type CommentVotedEdit (line 93) | type CommentVotedEdit = {
type CriterionModifier (line 98) | enum CriterionModifier {
type DateAccuracyEnum (line 117) | enum DateAccuracyEnum {
type DateCriterionInput (line 123) | type DateCriterionInput = {
type DeleteEditInput (line 128) | type DeleteEditInput = {
type DeleteFingerprintSubmissionsInput (line 133) | type DeleteFingerprintSubmissionsInput = {
type DownvoteOwnEdit (line 138) | type DownvoteOwnEdit = {
type Draft (line 143) | type Draft = {
type DraftData (line 151) | type DraftData = PerformerDraft | SceneDraft;
type DraftEntity (line 153) | type DraftEntity = {
type DraftEntityInput (line 159) | type DraftEntityInput = {
type DraftFingerprint (line 164) | type DraftFingerprint = {
type DraftSubmissionStatus (line 171) | type DraftSubmissionStatus = {
type Edit (line 176) | type Edit = {
type EditComment (line 208) | type EditComment = {
type EditCommentInput (line 217) | type EditCommentInput = {
type EditDetails (line 222) | type EditDetails = PerformerEdit | SceneEdit | StudioEdit | TagEdit;
type EditInput (line 224) | type EditInput = {
type EditQueryInput (line 235) | type EditQueryInput = {
type EditSortEnum (line 264) | enum EditSortEnum {
type EditTarget (line 270) | type EditTarget = Performer | Scene | Studio | Tag;
type EditVote (line 272) | type EditVote = {
type EditVoteInput (line 279) | type EditVoteInput = {
type EthnicityEnum (line 284) | enum EthnicityEnum {
type EthnicityFilterEnum (line 295) | enum EthnicityFilterEnum {
type EyeColorCriterionInput (line 307) | type EyeColorCriterionInput = {
type EyeColorEnum (line 312) | enum EyeColorEnum {
type FailedOwnEdit (line 321) | type FailedOwnEdit = {
type FavoriteFilter (line 326) | enum FavoriteFilter {
type FavoritePerformerEdit (line 332) | type FavoritePerformerEdit = {
type FavoritePerformerScene (line 337) | type FavoritePerformerScene = {
type FavoriteStudioEdit (line 342) | type FavoriteStudioEdit = {
type FavoriteStudioScene (line 347) | type FavoriteStudioScene = {
type Fingerprint (line 352) | type Fingerprint = {
type FingerprintAlgorithm (line 369) | enum FingerprintAlgorithm {
type FingerprintBatchSubmission (line 375) | type FingerprintBatchSubmission = {
type FingerprintEditInput (line 382) | type FingerprintEditInput = {
type FingerprintInput (line 394) | type FingerprintInput = {
type FingerprintQueryInput (line 402) | type FingerprintQueryInput = {
type FingerprintSubmission (line 407) | type FingerprintSubmission = {
type FingerprintSubmissionResult (line 415) | type FingerprintSubmissionResult = {
type FingerprintSubmissionType (line 425) | enum FingerprintSubmissionType {
type FingerprintedSceneEdit (line 434) | type FingerprintedSceneEdit = {
type FuzzyDate (line 439) | type FuzzyDate = {
type GenderEnum (line 445) | enum GenderEnum {
type GenderFacet (line 454) | type GenderFacet = {
type GenderFilterEnum (line 460) | enum GenderFilterEnum {
type GenerateInviteCodeInput (line 470) | type GenerateInviteCodeInput = {
type GrantInviteInput (line 476) | type GrantInviteInput = {
type HairColorCriterionInput (line 481) | type HairColorCriterionInput = {
type HairColorEnum (line 486) | enum HairColorEnum {
type IdCriterionInput (line 499) | type IdCriterionInput = {
type Image (line 504) | type Image = {
type ImageCreateInput (line 512) | type ImageCreateInput = {
type ImageDestroyInput (line 517) | type ImageDestroyInput = {
type ImageUpdateInput (line 521) | type ImageUpdateInput = {
type IntCriterionInput (line 526) | type IntCriterionInput = {
type InviteKey (line 531) | type InviteKey = {
type MarkNotificationReadInput (line 538) | type MarkNotificationReadInput = {
type Measurements (line 543) | type Measurements = {
type ModAudit (line 551) | type ModAudit = {
type ModAuditActionEnum (line 563) | enum ModAuditActionEnum {
type ModAuditQueryInput (line 568) | type ModAuditQueryInput = {
type MoveFingerprintSubmissionsInput (line 575) | type MoveFingerprintSubmissionsInput = {
type MultiIdCriterionInput (line 581) | type MultiIdCriterionInput = {
type MultiStringCriterionInput (line 586) | type MultiStringCriterionInput = {
type Mutation (line 591) | type Mutation = {
type MutationActivateNewUserArgs (line 690) | type MutationActivateNewUserArgs = {
type MutationAmendEditArgs (line 695) | type MutationAmendEditArgs = {
type MutationApplyEditArgs (line 700) | type MutationApplyEditArgs = {
type MutationCancelEditArgs (line 705) | type MutationCancelEditArgs = {
type MutationChangePasswordArgs (line 710) | type MutationChangePasswordArgs = {
type MutationConfirmChangeEmailArgs (line 715) | type MutationConfirmChangeEmailArgs = {
type MutationDeleteEditArgs (line 720) | type MutationDeleteEditArgs = {
type MutationDestroyDraftArgs (line 725) | type MutationDestroyDraftArgs = {
type MutationEditCommentArgs (line 730) | type MutationEditCommentArgs = {
type MutationEditVoteArgs (line 735) | type MutationEditVoteArgs = {
type MutationFavoritePerformerArgs (line 740) | type MutationFavoritePerformerArgs = {
type MutationFavoriteStudioArgs (line 746) | type MutationFavoriteStudioArgs = {
type MutationGenerateInviteCodesArgs (line 752) | type MutationGenerateInviteCodesArgs = {
type MutationGrantInviteArgs (line 757) | type MutationGrantInviteArgs = {
type MutationImageCreateArgs (line 762) | type MutationImageCreateArgs = {
type MutationImageDestroyArgs (line 767) | type MutationImageDestroyArgs = {
type MutationMarkNotificationsReadArgs (line 772) | type MutationMarkNotificationsReadArgs = {
type MutationNewUserArgs (line 777) | type MutationNewUserArgs = {
type MutationPerformerCreateArgs (line 782) | type MutationPerformerCreateArgs = {
type MutationPerformerDestroyArgs (line 787) | type MutationPerformerDestroyArgs = {
type MutationPerformerEditArgs (line 792) | type MutationPerformerEditArgs = {
type MutationPerformerEditUpdateArgs (line 797) | type MutationPerformerEditUpdateArgs = {
type MutationPerformerUpdateArgs (line 803) | type MutationPerformerUpdateArgs = {
type MutationRegenerateApiKeyArgs (line 808) | type MutationRegenerateApiKeyArgs = {
type MutationRescindInviteCodeArgs (line 813) | type MutationRescindInviteCodeArgs = {
type MutationResetPasswordArgs (line 818) | type MutationResetPasswordArgs = {
type MutationRevokeInviteArgs (line 823) | type MutationRevokeInviteArgs = {
type MutationSceneCreateArgs (line 828) | type MutationSceneCreateArgs = {
type MutationSceneDeleteFingerprintSubmissionsArgs (line 833) | type MutationSceneDeleteFingerprintSubmissionsArgs = {
type MutationSceneDestroyArgs (line 838) | type MutationSceneDestroyArgs = {
type MutationSceneEditArgs (line 843) | type MutationSceneEditArgs = {
type MutationSceneEditUpdateArgs (line 848) | type MutationSceneEditUpdateArgs = {
type MutationSceneMoveFingerprintSubmissionsArgs (line 854) | type MutationSceneMoveFingerprintSubmissionsArgs = {
type MutationSceneUpdateArgs (line 859) | type MutationSceneUpdateArgs = {
type MutationSiteCreateArgs (line 864) | type MutationSiteCreateArgs = {
type MutationSiteDestroyArgs (line 869) | type MutationSiteDestroyArgs = {
type MutationSiteUpdateArgs (line 874) | type MutationSiteUpdateArgs = {
type MutationStudioCreateArgs (line 879) | type MutationStudioCreateArgs = {
type MutationStudioDestroyArgs (line 884) | type MutationStudioDestroyArgs = {
type MutationStudioEditArgs (line 889) | type MutationStudioEditArgs = {
type MutationStudioEditUpdateArgs (line 894) | type MutationStudioEditUpdateArgs = {
type MutationStudioUpdateArgs (line 900) | type MutationStudioUpdateArgs = {
type MutationSubmitFingerprintArgs (line 905) | type MutationSubmitFingerprintArgs = {
type MutationSubmitFingerprintsArgs (line 910) | type MutationSubmitFingerprintsArgs = {
type MutationSubmitPerformerDraftArgs (line 915) | type MutationSubmitPerformerDraftArgs = {
type MutationSubmitSceneDraftArgs (line 920) | type MutationSubmitSceneDraftArgs = {
type MutationTagCategoryCreateArgs (line 925) | type MutationTagCategoryCreateArgs = {
type MutationTagCategoryDestroyArgs (line 930) | type MutationTagCategoryDestroyArgs = {
type MutationTagCategoryUpdateArgs (line 935) | type MutationTagCategoryUpdateArgs = {
type MutationTagCreateArgs (line 940) | type MutationTagCreateArgs = {
type MutationTagDestroyArgs (line 945) | type MutationTagDestroyArgs = {
type MutationTagEditArgs (line 950) | type MutationTagEditArgs = {
type MutationTagEditUpdateArgs (line 955) | type MutationTagEditUpdateArgs = {
type MutationTagUpdateArgs (line 961) | type MutationTagUpdateArgs = {
type MutationUpdateNotificationSubscriptionsArgs (line 966) | type MutationUpdateNotificationSubscriptionsArgs = {
type MutationUserCreateArgs (line 971) | type MutationUserCreateArgs = {
type MutationUserDestroyArgs (line 976) | type MutationUserDestroyArgs = {
type MutationUserUpdateArgs (line 981) | type MutationUserUpdateArgs = {
type MutationValidateChangeEmailArgs (line 986) | type MutationValidateChangeEmailArgs = {
type NewUserInput (line 991) | type NewUserInput = {
type Notification (line 996) | type Notification = {
type NotificationData (line 1003) | type NotificationData = CommentCommentedEdit | CommentOwnEdit | CommentV...
type NotificationEnum (line 1005) | enum NotificationEnum {
type OperationEnum (line 1019) | enum OperationEnum {
type Performer (line 1026) | type Performer = {
type PerformerScenesArgs (line 1072) | type PerformerScenesArgs = {
type PerformerStudiosArgs (line 1077) | type PerformerStudiosArgs = {
type PerformerAppearance (line 1081) | type PerformerAppearance = {
type PerformerAppearanceInput (line 1088) | type PerformerAppearanceInput = {
type PerformerCreateInput (line 1094) | type PerformerCreateInput = {
type PerformerDestroyInput (line 1120) | type PerformerDestroyInput = {
type PerformerDraft (line 1124) | type PerformerDraft = {
type PerformerDraftInput (line 1148) | type PerformerDraftInput = {
type PerformerEdit (line 1171) | type PerformerEdit = {
type PerformerEditDetailsInput (line 1209) | type PerformerEditDetailsInput = {
type PerformerEditInput (line 1235) | type PerformerEditInput = {
type PerformerEditOptions (line 1243) | type PerformerEditOptions = {
type PerformerEditOptionsInput (line 1251) | type PerformerEditOptionsInput = {
type PerformerQueryInput (line 1258) | type PerformerQueryInput = {
type PerformerScenesInput (line 1299) | type PerformerScenesInput = {
type PerformerSearchFacets (line 1308) | type PerformerSearchFacets = {
type PerformerSearchFilter (line 1313) | type PerformerSearchFilter = {
type PerformerSortEnum (line 1318) | enum PerformerSortEnum {
type PerformerStudio (line 1330) | type PerformerStudio = {
type PerformerUpdateInput (line 1336) | type PerformerUpdateInput = {
type Query (line 1363) | type Query = {
type QueryFindDraftArgs (line 1415) | type QueryFindDraftArgs = {
type QueryFindEditArgs (line 1421) | type QueryFindEditArgs = {
type QueryFindPerformerArgs (line 1427) | type QueryFindPerformerArgs = {
type QueryFindSceneArgs (line 1433) | type QueryFindSceneArgs = {
type QueryFindScenesBySceneFingerprintsArgs (line 1439) | type QueryFindScenesBySceneFingerprintsArgs = {
type QueryFindSiteArgs (line 1445) | type QueryFindSiteArgs = {
type QueryFindStudioArgs (line 1451) | type QueryFindStudioArgs = {
type QueryFindTagArgs (line 1458) | type QueryFindTagArgs = {
type QueryFindTagCategoryArgs (line 1465) | type QueryFindTagCategoryArgs = {
type QueryFindTagOrAliasArgs (line 1471) | type QueryFindTagOrAliasArgs = {
type QueryFindUserArgs (line 1477) | type QueryFindUserArgs = {
type QueryQueryEditsArgs (line 1484) | type QueryQueryEditsArgs = {
type QueryQueryExistingPerformerArgs (line 1490) | type QueryQueryExistingPerformerArgs = {
type QueryQueryExistingSceneArgs (line 1496) | type QueryQueryExistingSceneArgs = {
type QueryQueryModAuditsArgs (line 1502) | type QueryQueryModAuditsArgs = {
type QueryQueryNotificationsArgs (line 1508) | type QueryQueryNotificationsArgs = {
type QueryQueryPerformersArgs (line 1514) | type QueryQueryPerformersArgs = {
type QueryQueryScenesArgs (line 1520) | type QueryQueryScenesArgs = {
type QueryQueryStudiosArgs (line 1526) | type QueryQueryStudiosArgs = {
type QueryQueryTagsArgs (line 1532) | type QueryQueryTagsArgs = {
type QueryQueryUsersArgs (line 1538) | type QueryQueryUsersArgs = {
type QuerySearchPerformerArgs (line 1544) | type QuerySearchPerformerArgs = {
type QuerySearchPerformersArgs (line 1551) | type QuerySearchPerformersArgs = {
type QuerySearchSceneArgs (line 1561) | type QuerySearchSceneArgs = {
type QuerySearchScenesArgs (line 1568) | type QuerySearchScenesArgs = {
type QuerySearchStudioArgs (line 1577) | type QuerySearchStudioArgs = {
type QuerySearchTagArgs (line 1584) | type QuerySearchTagArgs = {
type QueryEditsResultType (line 1589) | type QueryEditsResultType = {
type QueryExistingPerformerInput (line 1595) | type QueryExistingPerformerInput = {
type QueryExistingPerformerResult (line 1601) | type QueryExistingPerformerResult = {
type QueryExistingSceneInput (line 1607) | type QueryExistingSceneInput = {
type QueryExistingSceneResult (line 1613) | type QueryExistingSceneResult = {
type QueryModAuditsResultType (line 1619) | type QueryModAuditsResultType = {
type QueryNotificationsInput (line 1625) | type QueryNotificationsInput = {
type QueryNotificationsResult (line 1632) | type QueryNotificationsResult = {
type QueryPerformersResultType (line 1638) | type QueryPerformersResultType = {
type QueryScenesResultType (line 1646) | type QueryScenesResultType = {
type QuerySitesResultType (line 1652) | type QuerySitesResultType = {
type QueryStudiosResultType (line 1658) | type QueryStudiosResultType = {
type QueryTagCategoriesResultType (line 1664) | type QueryTagCategoriesResultType = {
type QueryTagsResultType (line 1670) | type QueryTagsResultType = {
type QueryUsersResultType (line 1676) | type QueryUsersResultType = {
type ResetPasswordInput (line 1682) | type ResetPasswordInput = {
type RevokeInviteInput (line 1686) | type RevokeInviteInput = {
type RoleCriterionInput (line 1691) | type RoleCriterionInput = {
type RoleEnum (line 1696) | enum RoleEnum {
type Scene (line 1712) | type Scene = {
type SceneFingerprintsArgs (line 1737) | type SceneFingerprintsArgs = {
type SceneCreateInput (line 1741) | type SceneCreateInput = {
type SceneDestroyInput (line 1757) | type SceneDestroyInput = {
type SceneDraft (line 1761) | type SceneDraft = {
type SceneDraftInput (line 1778) | type SceneDraftInput = {
type SceneDraftPerformer (line 1796) | type SceneDraftPerformer = DraftEntity | Performer;
type SceneDraftStudio (line 1798) | type SceneDraftStudio = DraftEntity | Studio;
type SceneDraftTag (line 1800) | type SceneDraftTag = DraftEntity | Tag;
type SceneEdit (line 1802) | type SceneEdit = {
type SceneEditDetailsInput (line 1831) | type SceneEditDetailsInput = {
type SceneEditInput (line 1848) | type SceneEditInput = {
type SceneQueryInput (line 1854) | type SceneQueryInput = {
type SceneSortEnum (line 1887) | enum SceneSortEnum {
type SceneUpdateInput (line 1895) | type SceneUpdateInput = {
type Site (line 1912) | type Site = {
type SiteCreateInput (line 1925) | type SiteCreateInput = {
type SiteDestroyInput (line 1933) | type SiteDestroyInput = {
type SiteUpdateInput (line 1937) | type SiteUpdateInput = {
type SortDirectionEnum (line 1946) | enum SortDirectionEnum {
type StashBoxConfig (line 1951) | type StashBoxConfig = {
type StringCriterionInput (line 1967) | type StringCriterionInput = {
type Studio (line 1972) | type Studio = {
type StudioPerformersArgs (line 1991) | type StudioPerformersArgs = {
type StudioSub_StudiosArgs (line 1996) | type StudioSub_StudiosArgs = {
type StudioCreateInput (line 2000) | type StudioCreateInput = {
type StudioDestroyInput (line 2008) | type StudioDestroyInput = {
type StudioEdit (line 2012) | type StudioEdit = {
type StudioEditDetailsInput (line 2027) | type StudioEditDetailsInput = {
type StudioEditInput (line 2035) | type StudioEditInput = {
type StudioQueryInput (line 2041) | type StudioQueryInput = {
type StudioSortEnum (line 2058) | enum StudioSortEnum {
type StudioUpdateInput (line 2064) | type StudioUpdateInput = {
type Tag (line 2073) | type Tag = {
type TagCategory (line 2086) | type TagCategory = {
type TagCategoryCreateInput (line 2094) | type TagCategoryCreateInput = {
type TagCategoryDestroyInput (line 2100) | type TagCategoryDestroyInput = {
type TagCategoryUpdateInput (line 2104) | type TagCategoryUpdateInput = {
type TagCreateInput (line 2111) | type TagCreateInput = {
type TagDestroyInput (line 2118) | type TagDestroyInput = {
type TagEdit (line 2122) | type TagEdit = {
type TagEditDetailsInput (line 2132) | type TagEditDetailsInput = {
type TagEditInput (line 2139) | type TagEditInput = {
type TagGroupEnum (line 2145) | enum TagGroupEnum {
type TagQueryInput (line 2151) | type TagQueryInput = {
type TagSortEnum (line 2166) | enum TagSortEnum {
type TagUpdateInput (line 2172) | type TagUpdateInput = {
type TargetTypeEnum (line 2180) | enum TargetTypeEnum {
type Url (line 2187) | type Url = {
type UrlInput (line 2195) | type UrlInput = {
type UpdatedEdit (line 2200) | type UpdatedEdit = {
type User (line 2205) | type User = {
type UserChangeEmailInput (line 2229) | type UserChangeEmailInput = {
type UserChangeEmailStatus (line 2235) | enum UserChangeEmailStatus {
type UserChangePasswordInput (line 2244) | type UserChangePasswordInput = {
type UserCreateInput (line 2251) | type UserCreateInput = {
type UserDestroyInput (line 2260) | type UserDestroyInput = {
type UserEditCount (line 2264) | type UserEditCount = {
type UserQueryInput (line 2275) | type UserQueryInput = {
type UserUpdateInput (line 2300) | type UserUpdateInput = {
type UserVoteCount (line 2309) | type UserVoteCount = {
type UserVotedFilterEnum (line 2318) | enum UserVotedFilterEnum {
type ValidSiteTypeEnum (line 2325) | enum ValidSiteTypeEnum {
type Version (line 2331) | type Version = {
type VoteStatusEnum (line 2339) | enum VoteStatusEnum {
type VoteTypeEnum (line 2349) | enum VoteTypeEnum {
type CommentFragment (line 2359) | type CommentFragment = { __typename: 'EditComment', id: string, date: st...
type EditFragment (line 2361) | type EditFragment = { __typename: 'Edit', id: string, target_type: Targe...
type FingerprintFragment (line 2383) | type FingerprintFragment = { __typename: 'Fingerprint', hash: string, al...
type ImageFragment (line 2385) | type ImageFragment = { __typename: 'Image', id: string, url: string, wid...
type PerformerFragment (line 2387) | type PerformerFragment = { __typename: 'Performer', id: string, name: st...
type QuerySceneFragment (line 2389) | type QuerySceneFragment = { __typename: 'Scene', id: string, release_dat...
type SceneFragment (line 2391) | type SceneFragment = { __typename: 'Scene', id: string, release_date?: s...
type ScenePerformerFragment (line 2393) | type ScenePerformerFragment = { __typename: 'Performer', id: string, nam...
type SearchPerformerFragment (line 2395) | type SearchPerformerFragment = { __typename: 'Performer', id: string, na...
type StudioFragment (line 2397) | type StudioFragment = { __typename: 'Studio', id: string, name: string, ...
type TagFragment (line 2399) | type TagFragment = { __typename: 'Tag', id: string, name: string, descri...
type UrlFragment (line 2401) | type UrlFragment = { __typename: 'URL', url: string, site: { __typename:...
type ActivateNewUserMutationVariables (line 2403) | type ActivateNewUserMutationVariables = Exact<{
type ActivateNewUserMutation (line 2408) | type ActivateNewUserMutation = { __typename: 'Mutation', activateNewUser...
type AddImageMutationVariables (line 2410) | type AddImageMutationVariables = Exact<{
type AddImageMutation (line 2415) | type AddImageMutation = { __typename: 'Mutation', imageCreate?: { __type...
type AddSceneMutationVariables (line 2417) | type AddSceneMutationVariables = Exact<{
type AddSceneMutation (line 2422) | type AddSceneMutation = { __typename: 'Mutation', sceneCreate?: { __type...
type AddSiteMutationVariables (line 2424) | type AddSiteMutationVariables = Exact<{
type AddSiteMutation (line 2429) | type AddSiteMutation = { __typename: 'Mutation', siteCreate?: { __typena...
type AddStudioMutationVariables (line 2431) | type AddStudioMutationVariables = Exact<{
type AddStudioMutation (line 2436) | type AddStudioMutation = { __typename: 'Mutation', studioCreate?: { __ty...
type AddTagCategoryMutationVariables (line 2438) | type AddTagCategoryMutationVariables = Exact<{
type AddTagCategoryMutation (line 2443) | type AddTagCategoryMutation = { __typename: 'Mutation', tagCategoryCreat...
type AddUserMutationVariables (line 2445) | type AddUserMutationVariables = Exact<{
type AddUserMutation (line 2450) | type AddUserMutation = { __typename: 'Mutation', userCreate?: { __typena...
type AmendEditMutationVariables (line 2452) | type AmendEditMutationVariables = Exact<{
type AmendEditMutation (line 2457) | type AmendEditMutation = { __typename: 'Mutation', amendEdit: { __typena...
type ApplyEditMutationVariables (line 2479) | type ApplyEditMutationVariables = Exact<{
type ApplyEditMutation (line 2484) | type ApplyEditMutation = { __typename: 'Mutation', applyEdit: { __typena...
type CancelEditMutationVariables (line 2506) | type CancelEditMutationVariables = Exact<{
type CancelEditMutation (line 2511) | type CancelEditMutation = { __typename: 'Mutation', cancelEdit: { __type...
type ChangePasswordMutationVariables (line 2528) | type ChangePasswordMutationVariables = Exact<{
type ChangePasswordMutation (line 2533) | type ChangePasswordMutation = { __typename: 'Mutation', changePassword: ...
type ConfirmChangeEmailMutationVariables (line 2535) | type ConfirmChangeEmailMutationVariables = Exact<{
type ConfirmChangeEmailMutation (line 2540) | type ConfirmChangeEmailMutation = { __typename: 'Mutation', confirmChang...
type DeleteDraftMutationVariables (line 2542) | type DeleteDraftMutationVariables = Exact<{
type DeleteDraftMutation (line 2547) | type DeleteDraftMutation = { __typename: 'Mutation', destroyDraft: boole...
type DeleteEditMutationVariables (line 2549) | type DeleteEditMutationVariables = Exact<{
type DeleteEditMutation (line 2554) | type DeleteEditMutation = { __typename: 'Mutation', deleteEdit: boolean };
type DeleteFingerprintSubmissionsMutationVariables (line 2556) | type DeleteFingerprintSubmissionsMutationVariables = Exact<{
type DeleteFingerprintSubmissionsMutation (line 2561) | type DeleteFingerprintSubmissionsMutation = { __typename: 'Mutation', sc...
type DeleteSceneMutationVariables (line 2563) | type DeleteSceneMutationVariables = Exact<{
type DeleteSceneMutation (line 2568) | type DeleteSceneMutation = { __typename: 'Mutation', sceneDestroy: boole...
type DeleteSiteMutationVariables (line 2570) | type DeleteSiteMutationVariables = Exact<{
type DeleteSiteMutation (line 2575) | type DeleteSiteMutation = { __typename: 'Mutation', siteDestroy: boolean };
type DeleteStudioMutationVariables (line 2577) | type DeleteStudioMutationVariables = Exact<{
type DeleteStudioMutation (line 2582) | type DeleteStudioMutation = { __typename: 'Mutation', studioDestroy: boo...
type DeleteTagCategoryMutationVariables (line 2584) | type DeleteTagCategoryMutationVariables = Exact<{
type DeleteTagCategoryMutation (line 2589) | type DeleteTagCategoryMutation = { __typename: 'Mutation', tagCategoryDe...
type DeleteUserMutationVariables (line 2591) | type DeleteUserMutationVariables = Exact<{
type DeleteUserMutation (line 2596) | type DeleteUserMutation = { __typename: 'Mutation', userDestroy: boolean };
type EditCommentMutationVariables (line 2598) | type EditCommentMutationVariables = Exact<{
type EditCommentMutation (line 2603) | type EditCommentMutation = { __typename: 'Mutation', editComment: { __ty...
type FavoritePerformerMutationVariables (line 2605) | type FavoritePerformerMutationVariables = Exact<{
type FavoritePerformerMutation (line 2611) | type FavoritePerformerMutation = { __typename: 'Mutation', favoritePerfo...
type FavoriteStudioMutationVariables (line 2613) | type FavoriteStudioMutationVariables = Exact<{
type FavoriteStudioMutation (line 2619) | type FavoriteStudioMutation = { __typename: 'Mutation', favoriteStudio: ...
type GenerateInviteCodesMutationVariables (line 2621) | type GenerateInviteCodesMutationVariables = Exact<{
type GenerateInviteCodesMutation (line 2626) | type GenerateInviteCodesMutation = { __typename: 'Mutation', generateInv...
type GrantInviteMutationVariables (line 2628) | type GrantInviteMutationVariables = Exact<{
type GrantInviteMutation (line 2633) | type GrantInviteMutation = { __typename: 'Mutation', grantInvite: number };
type MarkNotificationReadMutationVariables (line 2635) | type MarkNotificationReadMutationVariables = Exact<{
type MarkNotificationReadMutation (line 2640) | type MarkNotificationReadMutation = { __typename: 'Mutation', markNotifi...
type MarkNotificationsReadMutationVariables (line 2642) | type MarkNotificationsReadMutationVariables = Exact<{ [key: string]: nev...
type MarkNotificationsReadMutation (line 2645) | type MarkNotificationsReadMutation = { __typename: 'Mutation', markNotif...
type MoveFingerprintSubmissionsMutationVariables (line 2647) | type MoveFingerprintSubmissionsMutationVariables = Exact<{
type MoveFingerprintSubmissionsMutation (line 2652) | type MoveFingerprintSubmissionsMutation = { __typename: 'Mutation', scen...
type NewUserMutationVariables (line 2654) | type NewUserMutationVariables = Exact<{
type NewUserMutation (line 2659) | type NewUserMutation = { __typename: 'Mutation', newUser?: string | null };
type PerformerEditMutationVariables (line 2661) | type PerformerEditMutationVariables = Exact<{
type PerformerEditMutation (line 2666) | type PerformerEditMutation = { __typename: 'Mutation', performerEdit: { ...
type PerformerEditUpdateMutationVariables (line 2688) | type PerformerEditUpdateMutationVariables = Exact<{
type PerformerEditUpdateMutation (line 2694) | type PerformerEditUpdateMutation = { __typename: 'Mutation', performerEd...
type RegenerateApiKeyMutationVariables (line 2716) | type RegenerateApiKeyMutationVariables = Exact<{
type RegenerateApiKeyMutation (line 2721) | type RegenerateApiKeyMutation = { __typename: 'Mutation', regenerateAPIK...
type RequestChangeEmailMutationVariables (line 2723) | type RequestChangeEmailMutationVariables = Exact<{ [key: string]: never;...
type RequestChangeEmailMutation (line 2726) | type RequestChangeEmailMutation = { __typename: 'Mutation', requestChang...
type RescindInviteCodeMutationVariables (line 2728) | type RescindInviteCodeMutationVariables = Exact<{
type RescindInviteCodeMutation (line 2733) | type RescindInviteCodeMutation = { __typename: 'Mutation', rescindInvite...
type ResetPasswordMutationVariables (line 2735) | type ResetPasswordMutationVariables = Exact<{
type ResetPasswordMutation (line 2740) | type ResetPasswordMutation = { __typename: 'Mutation', resetPassword: bo...
type RevokeInviteMutationVariables (line 2742) | type RevokeInviteMutationVariables = Exact<{
type RevokeInviteMutation (line 2747) | type RevokeInviteMutation = { __typename: 'Mutation', revokeInvite: numb...
type SceneEditMutationVariables (line 2749) | type SceneEditMutationVariables = Exact<{
type SceneEditMutation (line 2754) | type SceneEditMutation = { __typename: 'Mutation', sceneEdit: { __typena...
type SceneEditUpdateMutationVariables (line 2776) | type SceneEditUpdateMutationVariables = Exact<{
type SceneEditUpdateMutation (line 2782) | type SceneEditUpdateMutation = { __typename: 'Mutation', sceneEditUpdate...
type StudioEditMutationVariables (line 2804) | type StudioEditMutationVariables = Exact<{
type StudioEditMutation (line 2809) | type StudioEditMutation = { __typename: 'Mutation', studioEdit: { __type...
type StudioEditUpdateMutationVariables (line 2831) | type StudioEditUpdateMutationVariables = Exact<{
type StudioEditUpdateMutation (line 2837) | type StudioEditUpdateMutation = { __typename: 'Mutation', studioEditUpda...
type TagEditMutationVariables (line 2859) | type TagEditMutationVariables = Exact<{
type TagEditMutation (line 2864) | type TagEditMutation = { __typename: 'Mutation', tagEdit: { __typename: ...
type TagEditUpdateMutationVariables (line 2886) | type TagEditUpdateMutationVariables = Exact<{
type TagEditUpdateMutation (line 2892) | type TagEditUpdateMutation = { __typename: 'Mutation', tagEditUpdate: { ...
type UnmatchFingerprintMutationVariables (line 2914) | type UnmatchFingerprintMutationVariables = Exact<{
type UnmatchFingerprintMutation (line 2922) | type UnmatchFingerprintMutation = { __typename: 'Mutation', unmatchFinge...
type UpdateNotificationSubscriptionsMutationVariables (line 2924) | type UpdateNotificationSubscriptionsMutationVariables = Exact<{
type UpdateNotificationSubscriptionsMutation (line 2929) | type UpdateNotificationSubscriptionsMutation = { __typename: 'Mutation',...
type UpdateSceneMutationVariables (line 2931) | type UpdateSceneMutationVariables = Exact<{
type UpdateSceneMutation (line 2936) | type UpdateSceneMutation = { __typename: 'Mutation', sceneUpdate?: { __t...
type UpdateSiteMutationVariables (line 2938) | type UpdateSiteMutationVariables = Exact<{
type UpdateSiteMutation (line 2943) | type UpdateSiteMutation = { __typename: 'Mutation', siteUpdate?: { __typ...
type UpdateStudioMutationVariables (line 2945) | type UpdateStudioMutationVariables = Exact<{
type UpdateStudioMutation (line 2950) | type UpdateStudioMutation = { __typename: 'Mutation', studioUpdate?: { _...
type UpdateTagCategoryMutationVariables (line 2952) | type UpdateTagCategoryMutationVariables = Exact<{
type UpdateTagCategoryMutation (line 2957) | type UpdateTagCategoryMutation = { __typename: 'Mutation', tagCategoryUp...
type UpdateUserMutationVariables (line 2959) | type UpdateUserMutationVariables = Exact<{
type UpdateUserMutation (line 2964) | type UpdateUserMutation = { __typename: 'Mutation', userUpdate?: { __typ...
type ValidateChangeEmailMutationVariables (line 2966) | type ValidateChangeEmailMutationVariables = Exact<{
type ValidateChangeEmailMutation (line 2972) | type ValidateChangeEmailMutation = { __typename: 'Mutation', validateCha...
type VoteMutationVariables (line 2974) | type VoteMutationVariables = Exact<{
type VoteMutation (line 2979) | type VoteMutation = { __typename: 'Mutation', editVote: { __typename: 'E...
type CategoriesQueryVariables (line 3001) | type CategoriesQueryVariables = Exact<{ [key: string]: never; }>;
type CategoriesQuery (line 3004) | type CategoriesQuery = { __typename: 'Query', queryTagCategories: { __ty...
type CategoryQueryVariables (line 3006) | type CategoryQueryVariables = Exact<{
type CategoryQuery (line 3011) | type CategoryQuery = { __typename: 'Query', findTagCategory?: { __typena...
type ConfigQueryVariables (line 3013) | type ConfigQueryVariables = Exact<{ [key: string]: never; }>;
type ConfigQuery (line 3016) | type ConfigQuery = { __typename: 'Query', getConfig: { __typename: 'Stas...
type DraftQueryVariables (line 3018) | type DraftQueryVariables = Exact<{
type DraftQuery (line 3023) | type DraftQuery = { __typename: 'Query', findDraft?: { __typename: 'Draf...
type DraftsQueryVariables (line 3037) | type DraftsQueryVariables = Exact<{ [key: string]: never; }>;
type DraftsQuery (line 3040) | type DraftsQuery = { __typename: 'Query', findDrafts: Array<{ __typename...
type EditQueryVariables (line 3045) | type EditQueryVariables = Exact<{
type EditQuery (line 3050) | type EditQuery = { __typename: 'Query', findEdit?: { __typename: 'Edit',...
type EditUpdateQueryVariables (line 3072) | type EditUpdateQueryVariables = Exact<{
type EditUpdateQuery (line 3077) | type EditUpdateQuery = { __typename: 'Query', findEdit?: { __typename: '...
type EditsQueryVariables (line 3094) | type EditsQueryVariables = Exact<{
type EditsQuery (line 3099) | type EditsQuery = { __typename: 'Query', queryEdits: { __typename: 'Quer...
type FullPerformerQueryVariables (line 3121) | type FullPerformerQueryVariables = Exact<{
type FullPerformerQuery (line 3126) | type FullPerformerQuery = { __typename: 'Query', findPerformer?: { __typ...
type MeQueryVariables (line 3128) | type MeQueryVariables = Exact<{ [key: string]: never; }>;
type MeQuery (line 3131) | type MeQuery = { __typename: 'Query', me?: { __typename: 'User', id: str...
type ModAuditsQueryVariables (line 3133) | type ModAuditsQueryVariables = Exact<{
type ModAuditsQuery (line 3138) | type ModAuditsQuery = { __typename: 'Query', queryModAudits: { __typenam...
type PendingEditsCountQueryVariables (line 3140) | type PendingEditsCountQueryVariables = Exact<{
type PendingEditsCountQuery (line 3146) | type PendingEditsCountQuery = { __typename: 'Query', queryEdits: { __typ...
type PerformerQueryVariables (line 3148) | type PerformerQueryVariables = Exact<{
type PerformerQuery (line 3153) | type PerformerQuery = { __typename: 'Query', findPerformer?: { __typenam...
type PerformersQueryVariables (line 3155) | type PerformersQueryVariables = Exact<{
type PerformersQuery (line 3160) | type PerformersQuery = { __typename: 'Query', queryPerformers: { __typen...
type PublicUserQueryVariables (line 3162) | type PublicUserQueryVariables = Exact<{
type PublicUserQuery (line 3167) | type PublicUserQuery = { __typename: 'Query', findUser?: { __typename: '...
type QueryExistingPerformerQueryVariables (line 3169) | type QueryExistingPerformerQueryVariables = Exact<{
type QueryExistingPerformerQuery (line 3174) | type QueryExistingPerformerQuery = { __typename: 'Query', queryExistingP...
type QueryExistingSceneQueryVariables (line 3196) | type QueryExistingSceneQueryVariables = Exact<{
type QueryExistingSceneQuery (line 3201) | type QueryExistingSceneQuery = { __typename: 'Query', queryExistingScene...
type NotificationCommentFragment (line 3223) | type NotificationCommentFragment = { __typename: 'EditComment', id: stri...
type NotificationsQueryVariables (line 3245) | type NotificationsQueryVariables = Exact<{
type NotificationsQuery (line 3250) | type NotificationsQuery = { __typename: 'Query', queryNotifications: { _...
type SceneQueryVariables (line 3444) | type SceneQueryVariables = Exact<{
type SceneQuery (line 3449) | type SceneQuery = { __typename: 'Query', findScene?: { __typename: 'Scen...
type ScenePairingsQueryVariables (line 3451) | type ScenePairingsQueryVariables = Exact<{
type ScenePairingsQuery (line 3464) | type ScenePairingsQuery = { __typename: 'Query', queryPerformers: { __ty...
type ScenesQueryVariables (line 3466) | type ScenesQueryVariables = Exact<{
type ScenesQuery (line 3471) | type ScenesQuery = { __typename: 'Query', queryScenes: { __typename: 'Qu...
type ScenesWithFingerprintsQueryVariables (line 3473) | type ScenesWithFingerprintsQueryVariables = Exact<{
type ScenesWithFingerprintsQuery (line 3479) | type ScenesWithFingerprintsQuery = { __typename: 'Query', queryScenes: {...
type ScenesWithoutCountQueryVariables (line 3481) | type ScenesWithoutCountQueryVariables = Exact<{
type ScenesWithoutCountQuery (line 3486) | type ScenesWithoutCountQuery = { __typename: 'Query', queryScenes: { __t...
type SearchAllQueryVariables (line 3488) | type SearchAllQueryVariables = Exact<{
type SearchAllQuery (line 3494) | type SearchAllQuery = { __typename: 'Query', searchPerformers: { __typen...
type SearchPerformersQueryVariables (line 3496) | type SearchPerformersQueryVariables = Exact<{
type SearchPerformersQuery (line 3506) | type SearchPerformersQuery = { __typename: 'Query', searchPerformers: { ...
type SearchScenesQueryVariables (line 3508) | type SearchScenesQueryVariables = Exact<{
type SearchScenesQuery (line 3515) | type SearchScenesQuery = { __typename: 'Query', searchScenes: { __typena...
type SearchTagFragment (line 3517) | type SearchTagFragment = { __typename: 'Tag', deleted: boolean, id: stri...
type SearchTagsQueryVariables (line 3519) | type SearchTagsQueryVariables = Exact<{
type SearchTagsQuery (line 3525) | type SearchTagsQuery = { __typename: 'Query', exact?: { __typename: 'Tag...
type SiteQueryVariables (line 3527) | type SiteQueryVariables = Exact<{
type SiteQuery (line 3532) | type SiteQuery = { __typename: 'Query', findSite?: { __typename: 'Site',...
type SitesQueryVariables (line 3534) | type SitesQueryVariables = Exact<{ [key: string]: never; }>;
type SitesQuery (line 3537) | type SitesQuery = { __typename: 'Query', querySites: { __typename: 'Quer...
type StudioQueryVariables (line 3539) | type StudioQueryVariables = Exact<{
type StudioQuery (line 3544) | type StudioQuery = { __typename: 'Query', findStudio?: { __typename: 'St...
type StudioPerformersQueryVariables (line 3546) | type StudioPerformersQueryVariables = Exact<{
type StudioPerformersQuery (line 3558) | type StudioPerformersQuery = { __typename: 'Query', queryPerformers: { _...
type StudiosQueryVariables (line 3560) | type StudiosQueryVariables = Exact<{
type StudiosQuery (line 3565) | type StudiosQuery = { __typename: 'Query', queryStudios: { __typename: '...
type SubStudiosQueryVariables (line 3567) | type SubStudiosQueryVariables = Exact<{
type SubStudiosQuery (line 3573) | type SubStudiosQuery = { __typename: 'Query', findStudio?: { __typename:...
type TagQueryVariables (line 3575) | type TagQueryVariables = Exact<{
type TagQuery (line 3581) | type TagQuery = { __typename: 'Query', findTag?: { __typename: 'Tag', id...
type TagsQueryVariables (line 3583) | type TagsQueryVariables = Exact<{
type TagsQuery (line 3588) | type TagsQuery = { __typename: 'Query', queryTags: { __typename: 'QueryT...
type UnreadNotificationCountQueryVariables (line 3590) | type UnreadNotificationCountQueryVariables = Exact<{ [key: string]: neve...
type UnreadNotificationCountQuery (line 3593) | type UnreadNotificationCountQuery = { __typename: 'Query', getUnreadNoti...
type UserQueryVariables (line 3595) | type UserQueryVariables = Exact<{
type UserQuery (line 3600) | type UserQuery = { __typename: 'Query', findUser?: { __typename: 'User',...
type UsersQueryVariables (line 3602) | type UsersQueryVariables = Exact<{
type UsersQuery (line 3607) | type UsersQuery = { __typename: 'Query', queryUsers: { __typename: 'Quer...
type VersionQueryVariables (line 3609) | type VersionQueryVariables = Exact<{ [key: string]: never; }>;
type VersionQuery (line 3612) | type VersionQuery = { __typename: 'Query', version: { __typename: 'Versi...
FILE: frontend/src/hooks/useAuth.tsx
type AuthResult (line 5) | interface AuthResult {
FILE: frontend/src/hooks/useEditFilter.tsx
type EditFilterProps (line 47) | interface EditFilterProps {
FILE: frontend/src/hooks/useQueryParams.ts
type ParamBase (line 7) | interface ParamBase {
type StringParamConfig (line 10) | interface StringParamConfig extends ParamBase {
type StringArrayParamConfig (line 14) | interface StringArrayParamConfig extends ParamBase {
type NumberParamConfig (line 18) | interface NumberParamConfig extends ParamBase {
type NumberArrayParamConfig (line 22) | interface NumberArrayParamConfig extends ParamBase {
type BooleanParamConfig (line 26) | interface BooleanParamConfig extends ParamBase {
type ParamConfig (line 30) | type ParamConfig =
type QueryParamConfig (line 36) | type QueryParamConfig = Record<string, ParamConfig>;
type QueryParams (line 38) | type QueryParams<T extends QueryParamConfig> = {
type SetParams (line 52) | type SetParams<T extends QueryParamConfig, K extends keyof T> = (
type SetParamsCallback (line 56) | type SetParamsCallback<T extends QueryParamConfig> = SetParams<
FILE: frontend/src/hooks/useToast.tsx
type Message (line 4) | interface Message {
constant DISPLAY_TIME (line 10) | const DISPLAY_TIME = 5000;
constant ANIMATION_TIME (line 11) | const ANIMATION_TIME = 1000;
type ToastsProps (line 35) | interface ToastsProps {
type Props (line 59) | interface Props {
FILE: frontend/src/modules.d.ts
type ImportMetaEnv (line 8) | interface ImportMetaEnv extends Readonly<Record<string, string>> {
type ImportMeta (line 13) | interface ImportMeta {
FILE: frontend/src/pages/activateUser/ActivateUser.tsx
type ActivateNewUserFormData (line 24) | type ActivateNewUserFormData = yup.InferType<typeof schema>;
function useQuery (line 26) | function useQuery() {
FILE: frontend/src/pages/audits/AmendmentAuditDetails.tsx
type EditAmendmentData (line 3) | interface EditAmendmentData {
function parseAmendmentData (line 8) | function parseAmendmentData(data: string): EditAmendmentData | null {
function formatValue (line 16) | function formatValue(value: unknown): string {
FILE: frontend/src/pages/audits/AuditRow.tsx
type AuditRowProps (line 21) | interface AuditRowProps {
FILE: frontend/src/pages/audits/Audits.tsx
constant PER_PAGE (line 11) | const PER_PAGE = 25;
FILE: frontend/src/pages/audits/DeleteAuditDetails.tsx
type EditDeleteData (line 5) | interface EditDeleteData {
FILE: frontend/src/pages/categories/Category.tsx
type Category (line 12) | type Category = NonNullable<CategoryQuery["findTagCategory"]>;
type Props (line 14) | interface Props {
FILE: frontend/src/pages/categories/CategoryEdit.tsx
type Category (line 12) | type Category = NonNullable<CategoryQuery["findTagCategory"]>;
type Props (line 14) | interface Props {
FILE: frontend/src/pages/categories/categoryForm/CategoryForm.tsx
type Category (line 17) | type Category = NonNullable<CategoryQuery["findTagCategory"]>;
type CategoryFormData (line 27) | type CategoryFormData = yup.Asserts<typeof schema>;
type TagProps (line 29) | interface TagProps {
FILE: frontend/src/pages/drafts/Draft.tsx
type Draft (line 5) | type Draft = NonNullable<DraftQuery["findDraft"]>;
FILE: frontend/src/pages/drafts/PerformerDraft.tsx
type Draft (line 16) | type Draft = NonNullable<DraftQuery["findDraft"]>;
type PerformerDraft (line 17) | type PerformerDraft = Draft["data"] & { __typename: "PerformerDraft" };
type Props (line 21) | interface Props {
FILE: frontend/src/pages/drafts/SceneDraft.tsx
type Draft (line 19) | type Draft = NonNullable<DraftQuery["findDraft"]>;
type SceneDraft (line 20) | type SceneDraft = Draft["data"] & { __typename: "SceneDraft" };
type Props (line 24) | interface Props {
FILE: frontend/src/pages/drafts/index.tsx
type DraftData (line 8) | type DraftData = NonNullable<DraftQuery["findDraft"]>["data"];
type SceneDraft (line 9) | type SceneDraft = DraftData & { __typename: "SceneDraft" };
type PerformerDraft (line 10) | type PerformerDraft = DraftData & { __typename: "PerformerDraft" };
FILE: frontend/src/pages/drafts/parse.ts
type DraftData (line 20) | type DraftData = NonNullable<DraftQuery["findDraft"]>["data"];
type SceneDraft (line 21) | type SceneDraft = DraftData & { __typename: "SceneDraft" };
type PerformerDraft (line 22) | type PerformerDraft = DraftData & { __typename: "PerformerDraft" };
type Tag (line 23) | type Tag = NonNullable<SceneQuery["findScene"]>["tags"][number];
type ScenePerformer (line 24) | type ScenePerformer = NonNullable<
type URL (line 28) | type URL = { url: string; site: { id: string; name: string; icon: string...
type Entity (line 38) | type Entity = { id: string };
type Performer (line 53) | type Performer = { performer: { id: string }; as?: string | null };
FILE: frontend/src/pages/edits/EditAmendForm.tsx
type EditAmendFormProps (line 15) | interface EditAmendFormProps {
FILE: frontend/src/pages/edits/components/DeleteEditModal.tsx
type Props (line 10) | interface Props {
FILE: frontend/src/pages/edits/components/UpdateCount.tsx
type Props (line 4) | interface Props {
FILE: frontend/src/pages/forgotPassword/ForgotPassword.tsx
type ResetPasswordFormData (line 18) | type ResetPasswordFormData = yup.Asserts<typeof schema>;
FILE: frontend/src/pages/home/Home.tsx
constant CLASSNAME (line 16) | const CLASSNAME = "HomePage";
constant CLASSNAME_SCENES (line 17) | const CLASSNAME_SCENES = `${CLASSNAME}-scenes`;
FILE: frontend/src/pages/notifications/CommentNotification.tsx
type Props (line 5) | interface Props {
FILE: frontend/src/pages/notifications/EditNotification.tsx
type Props (line 5) | interface Props {
FILE: frontend/src/pages/notifications/Notification.tsx
type Props (line 18) | interface Props {
FILE: frontend/src/pages/notifications/Notifications.tsx
constant PER_PAGE (line 18) | const PER_PAGE = 20;
FILE: frontend/src/pages/notifications/sceneNotification.tsx
type Props (line 5) | interface Props {
FILE: frontend/src/pages/notifications/types.ts
type NotificationType (line 3) | type NotificationType =
type CommentData (line 6) | type CommentData = Extract<NotificationType["data"], { comment: unknown }>;
type CommentNotificationType (line 7) | type CommentNotificationType = NotificationType & { data: CommentData };
type EditData (line 13) | type EditData = Extract<NotificationType["data"], { edit: unknown }>;
type EditNotificationType (line 14) | type EditNotificationType = NotificationType & { data: EditData };
type SceneData (line 20) | type SceneData = Extract<NotificationType["data"], { scene: unknown }>;
type SceneNotificationType (line 21) | type SceneNotificationType = NotificationType & { data: SceneData };
FILE: frontend/src/pages/performers/Performer.tsx
type Performer (line 19) | type Performer = NonNullable<FullPerformerQuery["findPerformer"]>;
constant DEFAULT_TAB (line 21) | const DEFAULT_TAB = "scenes";
type Props (line 23) | interface Props {
FILE: frontend/src/pages/performers/PerformerDelete.tsx
type Performer (line 16) | type Performer = NonNullable<FullPerformerQuery["findPerformer"]>;
type FormData (line 22) | type FormData = yup.Asserts<typeof schema>;
type Props (line 24) | interface Props {
FILE: frontend/src/pages/performers/PerformerEdit.tsx
type Performer (line 14) | type Performer = NonNullable<FullPerformerQuery["findPerformer"]>;
type Props (line 16) | interface Props {
FILE: frontend/src/pages/performers/PerformerEditUpdate.tsx
type EditUpdate (line 12) | type EditUpdate = NonNullable<EditUpdateQuery["findEdit"]>;
FILE: frontend/src/pages/performers/PerformerMerge.tsx
type Performer (line 20) | type Performer = NonNullable<FullPerformerQuery["findPerformer"]>;
type SearchPerformer (line 21) | type SearchPerformer = NonNullable<
constant UPDATE_ALIAS_MESSAGE (line 25) | const UPDATE_ALIAS_MESSAGE = `Enabling this option sets each merged perf...
constant CLASSNAME (line 29) | const CLASSNAME = "PerformerMerge";
type Props (line 31) | interface Props {
FILE: frontend/src/pages/performers/Performers.tsx
constant PER_PAGE (line 24) | const PER_PAGE = 25;
FILE: frontend/src/pages/performers/components/performerInfo.tsx
constant CLASSNAME (line 43) | const CLASSNAME = "PerformerInfo";
constant CLASSNAME_ACTIONS (line 44) | const CLASSNAME_ACTIONS = "PerformerInfo-actions";
type Props (line 46) | interface Props {
FILE: frontend/src/pages/performers/components/scenePairings.tsx
constant PER_PAGE (line 24) | const PER_PAGE = 25;
type Props (line 39) | interface Props {
FILE: frontend/src/pages/performers/performerForm/ExistingPerformerAlert.tsx
type Props (line 13) | interface Props {
FILE: frontend/src/pages/performers/performerForm/PerformerForm.tsx
type OptionEnum (line 50) | type OptionEnum = {
constant GENDER (line 61) | const GENDER: OptionEnum[] = [
constant HAIR (line 67) | const HAIR: OptionEnum[] = [
constant BREAST (line 81) | const BREAST: OptionEnum[] = [
constant EYE (line 88) | const EYE: OptionEnum[] = [
constant ETHNICITY (line 98) | const ETHNICITY: OptionEnum[] = [
constant UPDATE_ALIAS_MESSAGE (line 110) | const UPDATE_ALIAS_MESSAGE = `Enabling this option sets the current name...
type PerformerProps (line 120) | interface PerformerProps {
FILE: frontend/src/pages/performers/performerForm/schema.ts
type PerformerFormData (line 164) | type PerformerFormData = yup.Asserts<typeof PerformerSchema>;
FILE: frontend/src/pages/performers/performerForm/types.ts
type InitialPerformer (line 9) | type InitialPerformer = {
FILE: frontend/src/pages/registerUser/Register.tsx
type RegisterFormData (line 28) | type RegisterFormData = yup.Asserts<typeof schema>;
type Props (line 30) | interface Props {
FILE: frontend/src/pages/resetPassword/ResetPassword.tsx
type ResetPasswordFormData (line 40) | type ResetPasswordFormData = yup.Asserts<typeof schema>;
function useQuery (line 42) | function useQuery() {
FILE: frontend/src/pages/scenes/Scene.tsx
constant DEFAULT_TAB (line 27) | const DEFAULT_TAB = "description";
type Props (line 29) | interface Props {
FILE: frontend/src/pages/scenes/SceneDelete.tsx
type FormData (line 20) | type FormData = yup.Asserts<typeof schema>;
type Props (line 22) | interface Props {
FILE: frontend/src/pages/scenes/SceneEdit.tsx
type Props (line 14) | interface Props {
FILE: frontend/src/pages/scenes/SceneEditUpdate.tsx
type EditUpdate (line 12) | type EditUpdate = NonNullable<EditUpdateQuery["findEdit"]>;
FILE: frontend/src/pages/scenes/components/fingerprints/DeleteFingerprintsModal.tsx
type Props (line 6) | interface Props {
FILE: frontend/src/pages/scenes/components/fingerprints/FingerprintTableHeader.tsx
type Props (line 10) | interface Props {
FILE: frontend/src/pages/scenes/components/fingerprints/FingerprintTableRow.tsx
type Props (line 16) | interface Props {
FILE: frontend/src/pages/scenes/components/fingerprints/MoveFingerprintsModal.tsx
type Props (line 9) | interface Props {
FILE: frontend/src/pages/scenes/components/fingerprints/types.ts
type FingerprintTableProps (line 3) | interface FingerprintTableProps {
type MatchType (line 10) | type MatchType = "submission" | "report";
type SortColumn (line 12) | type SortColumn =
type SortDirection (line 21) | type SortDirection = "asc" | "desc";
FILE: frontend/src/pages/scenes/sceneForm/ExistingSceneAlert.tsx
type Props (line 9) | interface Props {
FILE: frontend/src/pages/scenes/sceneForm/SceneForm.tsx
constant CLASS_NAME (line 41) | const CLASS_NAME = "SceneForm";
constant CLASS_NAME_PERFORMER_CHANGE (line 42) | const CLASS_NAME_PERFORMER_CHANGE = `${CLASS_NAME}-performer-change`;
type SceneProps (line 44) | interface SceneProps {
FILE: frontend/src/pages/scenes/sceneForm/diff.ts
type OmittedKeys (line 18) | type OmittedKeys = "draft_id" | "added_fingerprints" | "removed_fingerpr...
type Performer (line 20) | type Performer = {
type Tag (line 28) | type Tag = {
FILE: frontend/src/pages/scenes/sceneForm/schema.ts
type SceneFormData (line 127) | type SceneFormData = yup.Asserts<typeof SceneSchema>;
FILE: frontend/src/pages/scenes/sceneForm/types.ts
type InitialScene (line 3) | type InitialScene = {
FILE: frontend/src/pages/search/GenderFacet.tsx
type Props (line 9) | interface Props {
FILE: frontend/src/pages/search/PerformerCard.tsx
type Performer (line 20) | type Performer = NonNullable<
FILE: frontend/src/pages/search/SceneCard.tsx
type Scene (line 14) | type Scene = NonNullable<
FILE: frontend/src/pages/search/SearchLayout.tsx
constant CLASSNAME (line 17) | const CLASSNAME = "SearchPage";
constant CLASSNAME_INPUT (line 18) | const CLASSNAME_INPUT = `${CLASSNAME}-input`;
FILE: frontend/src/pages/search/SearchPerformersTab.tsx
constant PER_PAGE (line 12) | const PER_PAGE = 20;
FILE: frontend/src/pages/search/SearchScenesTab.tsx
constant PER_PAGE (line 11) | const PER_PAGE = 20;
FILE: frontend/src/pages/sites/Site.tsx
type Site (line 12) | type Site = NonNullable<SiteQuery["findSite"]>;
type Props (line 14) | interface Props {
FILE: frontend/src/pages/sites/SiteEdit.tsx
type Site (line 12) | type Site = NonNullable<SiteQuery["findSite"]>;
type Props (line 14) | interface Props {
FILE: frontend/src/pages/sites/siteForm/SiteForm.tsx
type Site (line 17) | type Site = NonNullable<SiteQuery["findSite"]>;
type SiteFormData (line 32) | type SiteFormData = yup.Asserts<typeof schema>;
type SiteProps (line 34) | interface SiteProps {
FILE: frontend/src/pages/studios/Studio.tsx
type Studio (line 12) | type Studio = NonNullable<StudioQuery["findStudio"]>;
constant DEFAULT_TAB (line 31) | const DEFAULT_TAB = "scenes";
type Props (line 33) | interface Props {
FILE: frontend/src/pages/studios/StudioDelete.tsx
type FormData (line 20) | type FormData = yup.Asserts<typeof schema>;
type Props (line 22) | interface Props {
FILE: frontend/src/pages/studios/StudioEdit.tsx
type Studio (line 11) | type Studio = NonNullable<StudioQuery["findStudio"]>;
type Props (line 16) | interface Props {
FILE: frontend/src/pages/studios/StudioEditUpdate.tsx
type EditUpdate (line 12) | type EditUpdate = NonNullable<EditUpdateQuery["findEdit"]>;
FILE: frontend/src/pages/studios/Studios.tsx
constant PER_PAGE (line 13) | const PER_PAGE = 40;
FILE: frontend/src/pages/studios/components/studioPerformers.tsx
constant PER_PAGE (line 24) | const PER_PAGE = 25;
type Props (line 37) | interface Props {
FILE: frontend/src/pages/studios/components/subStudioList.tsx
constant PER_PAGE (line 11) | const PER_PAGE = 25;
type Props (line 13) | interface Props {
FILE: frontend/src/pages/studios/components/subStudioPreview.tsx
constant PREVIEW_COUNT (line 8) | const PREVIEW_COUNT = 25;
type Props (line 10) | interface Props {
FILE: frontend/src/pages/studios/studioForm/StudioForm.tsx
type StudioProps (line 29) | interface StudioProps {
FILE: frontend/src/pages/studios/studioForm/schema.ts
type StudioFormData (line 42) | type StudioFormData = yup.Asserts<typeof StudioSchema>;
FILE: frontend/src/pages/studios/studioForm/types.ts
type InitialStudio (line 1) | type InitialStudio = {
FILE: frontend/src/pages/tags/Tag.tsx
constant DEFAULT_TAB (line 23) | const DEFAULT_TAB = "scenes";
type Props (line 25) | interface Props {
FILE: frontend/src/pages/tags/TagDelete.tsx
type FormData (line 20) | type FormData = yup.Asserts<typeof schema>;
type Props (line 22) | interface Props {
FILE: frontend/src/pages/tags/TagEdit.tsx
type Props (line 15) | interface Props {
FILE: frontend/src/pages/tags/TagEditUpdate.tsx
type EditUpdate (line 12) | type EditUpdate = NonNullable<EditUpdateQuery["findEdit"]>;
FILE: frontend/src/pages/tags/TagMerge.tsx
type Props (line 17) | interface Props {
type TagSlim (line 21) | type TagSlim = {
FILE: frontend/src/pages/tags/tagForm/TagForm.tsx
type TagProps (line 23) | interface TagProps {
FILE: frontend/src/pages/tags/tagForm/schema.ts
type TagFormData (line 17) | type TagFormData = yup.Asserts<typeof TagSchema>;
FILE: frontend/src/pages/tags/tagForm/types.ts
type InitialTag (line 1) | type InitialTag = {
FILE: frontend/src/pages/users/GenerateInviteKeyModal.tsx
type ModalProps (line 6) | interface ModalProps {
FILE: frontend/src/pages/users/User.tsx
type IInviteKeys (line 42) | interface IInviteKeys {
type UserInviteKeysProps (line 48) | interface UserInviteKeysProps {
type User (line 102) | type User = NonNullable<UserQuery["findUser"]>;
type EditCounts (line 103) | type EditCounts = User["edit_count"];
type VoteCounts (line 104) | type VoteCounts = User["vote_count"];
type PublicUser (line 106) | type PublicUser = NonNullable<PublicUserQuery["findUser"]>;
type EditCount (line 108) | type EditCount = [VoteStatusEnum, number];
type VoteCount (line 122) | type VoteCount = [VoteTypeEnum, number];
type Props (line 133) | interface Props {
FILE: frontend/src/pages/users/UserEdit.tsx
type User (line 14) | type User =
type Props (line 18) | interface Props {
FILE: frontend/src/pages/users/UserEditForm.tsx
type UserFormData (line 20) | type UserFormData = yup.Asserts<typeof schema>;
type UserEditData (line 22) | type UserEditData = {
type UserProps (line 29) | interface UserProps {
FILE: frontend/src/pages/users/UserEdits.tsx
type Props (line 10) | interface Props {
FILE: frontend/src/pages/users/UserFingerprintsList/UserFingerprint.tsx
type Props (line 8) | interface Props {
FILE: frontend/src/pages/users/UserFingerprintsList/UserFingerprintsList.tsx
constant PER_PAGE (line 25) | const PER_PAGE = 20;
type Props (line 27) | interface Props {
FILE: frontend/src/pages/users/UserFingerprintsList/UserSceneLine.tsx
type Props (line 14) | interface Props {
FILE: frontend/src/pages/users/UserForm.tsx
type UserFormData (line 35) | type UserFormData = yup.Asserts<typeof schema>;
type UserData (line 37) | type UserData = {
type UserProps (line 44) | interface UserProps {
FILE: frontend/src/pages/users/UserNotificationPreferences.tsx
type Props (line 16) | interface Props {
FILE: frontend/src/pages/users/UserPasswordForm.tsx
type UserFormData (line 34) | type UserFormData = yup.InferType<typeof schema>;
type UserPasswordData (line 36) | type UserPasswordData = {
type UserProps (line 41) | interface UserProps {
FILE: frontend/src/pages/users/UserValidateChangeEmail.tsx
type ValidateChangeEmailFormData (line 19) | type ValidateChangeEmailFormData = yup.Asserts<typeof schema>;
FILE: frontend/src/pages/users/Users.tsx
constant PER_PAGE (line 18) | const PER_PAGE = 20;
FILE: frontend/src/utils/date.ts
constant MIN_DATE (line 32) | const MIN_DATE = Temporal.PlainDate.from("1900-01-01");
FILE: frontend/src/utils/edit.ts
type Edits (line 5) | type Edits = NonNullable<EditsQuery["queryEdits"]["edits"][number]>;
type Details (line 7) | type Details = Edits["details"];
type Target (line 9) | type Target = NonNullable<Edits["target"]>;
type Tag (line 10) | type Tag = Target & { __typename: "Tag" };
type Performer (line 11) | type Performer = Target & { __typename: "Performer" };
type Studio (line 12) | type Studio = Target & { __typename: "Studio" };
type Scene (line 13) | type Scene = Target & { __typename: "Scene" };
type TypeName (line 15) | interface TypeName {
FILE: frontend/src/utils/markdown.tsx
type Props (line 7) | interface Props {
FILE: frontend/src/utils/route.ts
constant ROUTES_WITH_ID (line 48) | const ROUTES_WITH_ID = [ROUTE_PERFORMER, ROUTE_SCENE, ROUTE_STUDIO, ROUT...
FILE: frontend/src/utils/transforms.ts
type Image (line 33) | type Image = {
FILE: frontend/src/utils/user.ts
type PrivateUser (line 4) | type PrivateUser = NonNullable<UserQuery["findUser"]>;
type PublicUser (line 5) | type PublicUser = NonNullable<PublicUserQuery["findUser"]>;
constant USER_STORAGE (line 7) | const USER_STORAGE = "stash_box_user";
FILE: internal/api/context_keys.go
type key (line 5) | type key
constant ContextRepo (line 8) | ContextRepo key = iota
FILE: internal/api/directives.go
function IsUserOwnerDirective (line 11) | func IsUserOwnerDirective(ctx context.Context, obj interface{}, next gra...
function HasRoleDirective (line 19) | func HasRoleDirective(ctx context.Context, obj interface{}, next graphql...
FILE: internal/api/draft_integration_test.go
type draftTestRunner (line 12) | type draftTestRunner struct
method testSubmitSceneDraft (line 22) | func (s *draftTestRunner) testSubmitSceneDraft() {
method testSubmitPerformerDraft (line 51) | func (s *draftTestRunner) testSubmitPerformerDraft() {
method testFindDraft (line 69) | func (s *draftTestRunner) testFindDraft() {
method testFindDrafts (line 89) | func (s *draftTestRunner) testFindDrafts() {
method testDestroyDraft (line 126) | func (s *draftTestRunner) testDestroyDraft() {
method testSceneDraftTagResolution (line 176) | func (s *draftTestRunner) testSceneDraftTagResolution() {
function createDraftTestRunner (line 16) | func createDraftTestRunner(t *testing.T) *draftTestRunner {
function TestSubmitSceneDraft (line 151) | func TestSubmitSceneDraft(t *testing.T) {
function TestSubmitPerformerDraft (line 156) | func TestSubmitPerformerDraft(t *testing.T) {
function TestFindDraft (line 161) | func TestFindDraft(t *testing.T) {
function TestFindDrafts (line 166) | func TestFindDrafts(t *testing.T) {
function TestDestroyDraft (line 171) | func TestDestroyDraft(t *testing.T) {
function TestSceneDraftTagResolution (line 253) | func TestSceneDraftTagResolution(t *testing.T) {
FILE: internal/api/edit_amend_integration_test.go
type editAmendTestRunner (line 12) | type editAmendTestRunner struct
method testAmendClosedEdit (line 22) | func (s *editAmendTestRunner) testAmendClosedEdit() {
method testAmendArrayItems (line 57) | func (s *editAmendTestRunner) testAmendArrayItems() {
method testCannotAmendPendingEdit (line 92) | func (s *editAmendTestRunner) testCannotAmendPendingEdit() {
method testNonModeratorCannotAmend (line 112) | func (s *editAmendTestRunner) testNonModeratorCannotAmend() {
method testAmendRequiresReason (line 137) | func (s *editAmendTestRunner) testAmendRequiresReason() {
method testAmendRequiresChanges (line 165) | func (s *editAmendTestRunner) testAmendRequiresChanges() {
function createEditAmendTestRunner (line 16) | func createEditAmendTestRunner(t *testing.T) *editAmendTestRunner {
function TestAmendClosedEdit (line 186) | func TestAmendClosedEdit(t *testing.T) {
function TestAmendArrayItems (line 191) | func TestAmendArrayItems(t *testing.T) {
function TestCannotAmendPendingEdit (line 196) | func TestCannotAmendPendingEdit(t *testing.T) {
function TestNonModeratorCannotAmendEdit (line 201) | func TestNonModeratorCannotAmendEdit(t *testing.T) {
function TestAmendRequiresReason (line 206) | func TestAmendRequiresReason(t *testing.T) {
function TestAmendRequiresChanges (line 211) | func TestAmendRequiresChanges(t *testing.T) {
FILE: internal/api/edit_delete_integration_test.go
type editDeleteTestRunner (line 12) | type editDeleteTestRunner struct
method testDeleteClosedEdit (line 22) | func (s *editDeleteTestRunner) testDeleteClosedEdit() {
method testCannotDeletePendingEdit (line 54) | func (s *editDeleteTestRunner) testCannotDeletePendingEdit() {
method testNonModeratorCannotDelete (line 80) | func (s *editDeleteTestRunner) testNonModeratorCannotDelete() {
method testDeleteRejectedEdit (line 111) | func (s *editDeleteTestRunner) testDeleteRejectedEdit() {
method testDeleteEditWithComments (line 142) | func (s *editDeleteTestRunner) testDeleteEditWithComments() {
method testDeleteEditWithVotes (line 183) | func (s *editDeleteTestRunner) testDeleteEditWithVotes() {
function createEditDeleteTestRunner (line 16) | func createEditDeleteTestRunner(t *testing.T) *editDeleteTestRunner {
function TestDeleteClosedEdit (line 224) | func TestDeleteClosedEdit(t *testing.T) {
function TestCannotDeletePendingEdit (line 229) | func TestCannotDeletePendingEdit(t *testing.T) {
function TestNonModeratorCannotDeleteEdit (line 234) | func TestNonModeratorCannotDeleteEdit(t *testing.T) {
function TestDeleteRejectedEdit (line 239) | func TestDeleteRejectedEdit(t *testing.T) {
function TestDeleteEditWithComments (line 244) | func TestDeleteEditWithComments(t *testing.T) {
function TestDeleteEditWithVotes (line 249) | func TestDeleteEditWithVotes(t *testing.T) {
FILE: internal/api/edit_integration_test.go
type editTestRunner (line 15) | type editTestRunner struct
method testAdminCancelEdit (line 25) | func (s *editTestRunner) testAdminCancelEdit() {
method verifyAdminCancelEdit (line 39) | func (s *editTestRunner) verifyAdminCancelEdit(edit *models.Edit) {
method testOwnerCancelEdit (line 46) | func (s *editTestRunner) testOwnerCancelEdit() {
method verifyOwnerCancelEdit (line 58) | func (s *editTestRunner) verifyOwnerCancelEdit(edit *models.Edit) {
method testEditComment (line 65) | func (s *editTestRunner) testEditComment() {
method verifyEditComment (line 79) | func (s *editTestRunner) verifyEditComment(edit *models.Edit, comment ...
method testVotePermissionsPromotion (line 85) | func (s *editTestRunner) testVotePermissionsPromotion() {
method verifyUserRolePromotion (line 109) | func (s *editTestRunner) verifyUserRolePromotion(user *models.User) {
method testPositiveEditVoteApplication (line 121) | func (s *editTestRunner) testPositiveEditVoteApplication() {
method verifyEditApplied (line 148) | func (s *editTestRunner) verifyEditApplied(edit *models.Edit) {
method verifyEditRejected (line 152) | func (s *editTestRunner) verifyEditRejected(edit *models.Edit) {
method testNegativeEditVoteApplication (line 156) | func (s *editTestRunner) testNegativeEditVoteApplication() {
method testEditVoteNotApplying (line 183) | func (s *editTestRunner) testEditVoteNotApplying() {
method verifyEditPending (line 219) | func (s *editTestRunner) verifyEditPending(edit *models.Edit) {
method testDestructiveEditsNotAutoApplied (line 223) | func (s *editTestRunner) testDestructiveEditsNotAutoApplied() {
method testVoteOwnedEditsDisallowed (line 251) | func (s *editTestRunner) testVoteOwnedEditsDisallowed() {
method testQueryEdits (line 311) | func (s *editTestRunner) testQueryEdits() {
method testBotFlag (line 574) | func (s *editTestRunner) testBotFlag() {
function createEditTestRunner (line 19) | func createEditTestRunner(t *testing.T) *editTestRunner {
function TestAdminCancelEdit (line 262) | func TestAdminCancelEdit(t *testing.T) {
function TestOwnerCancelEdit (line 269) | func TestOwnerCancelEdit(t *testing.T) {
function TestEditComment (line 276) | func TestEditComment(t *testing.T) {
function TestVotePermissionsPromotion (line 281) | func TestVotePermissionsPromotion(t *testing.T) {
function TestPositiveEditVoteApplication (line 286) | func TestPositiveEditVoteApplication(t *testing.T) {
function TestNegativeEditVoteApplication (line 291) | func TestNegativeEditVoteApplication(t *testing.T) {
function TestEditVoteNotApplying (line 296) | func TestEditVoteNotApplying(t *testing.T) {
function TestDestructiveEditsNotAutoApplied (line 301) | func TestDestructiveEditsNotAutoApplied(t *testing.T) {
function TestVoteOwnedEditsDisallowed (line 306) | func TestVoteOwnedEditsDisallowed(t *testing.T) {
function TestQueryEdits (line 569) | func TestQueryEdits(t *testing.T) {
function TestBotFlag (line 610) | func TestBotFlag(t *testing.T) {
FILE: internal/api/field_test.go
type fieldComparator (line 11) | type fieldComparator struct
method strPtrStrPtr (line 15) | func (c *fieldComparator) strPtrStrPtr(expected *string, actual *strin...
method strPtrNullStr (line 33) | func (c *fieldComparator) strPtrNullStr(expected *string, actual sql.N...
method strPtrNullUUID (line 51) | func (c *fieldComparator) strPtrNullUUID(expected *string, actual uuid...
method intPtrIntPtr (line 69) | func (c *fieldComparator) intPtrIntPtr(expected *int, actual *int, fie...
method uuidPtrUUIDPtr (line 87) | func (c *fieldComparator) uuidPtrUUIDPtr(expected *uuid.UUID, actual *...
method uuidPtrNullUUID (line 105) | func (c *fieldComparator) uuidPtrNullUUID(expected *uuid.UUID, actual ...
FILE: internal/api/fingerprint_filter.go
function filterMD5FingerprintInputs (line 6) | func filterMD5FingerprintInputs(fps []models.FingerprintInput) []models....
function filterMD5FingerprintQueryInputs (line 17) | func filterMD5FingerprintQueryInputs(fps [][]models.FingerprintQueryInpu...
function filterMD5FingerprintEditInputs (line 31) | func filterMD5FingerprintEditInputs(fps []models.FingerprintEditInput) [...
FILE: internal/api/graphql_client_test.go
type idObject (line 15) | type idObject struct
type performerAppearance (line 19) | type performerAppearance struct
type fingerprint (line 25) | type fingerprint struct
method FingerprintHash (line 35) | func (f fingerprint) FingerprintHash() models.FingerprintHash {
type siteURL (line 40) | type siteURL struct
type sceneOutput (line 45) | type sceneOutput struct
method UUID (line 63) | func (s sceneOutput) UUID() uuid.UUID {
type queryScenesResultType (line 67) | type queryScenesResultType struct
type measurements (line 72) | type measurements struct
type performerOutput (line 79) | type performerOutput struct
method UUID (line 97) | func (p performerOutput) UUID() uuid.UUID {
type studioOutput (line 101) | type studioOutput struct
method UUID (line 111) | func (s studioOutput) UUID() uuid.UUID {
type tagOutput (line 115) | type tagOutput struct
method UUID (line 125) | func (t tagOutput) UUID() uuid.UUID {
type siteOutput (line 129) | type siteOutput struct
method UUID (line 138) | func (s siteOutput) UUID() uuid.UUID {
type querySitesResultType (line 142) | type querySitesResultType struct
type queryPerformersResultType (line 146) | type queryPerformersResultType struct
type queryStudiosResultType (line 151) | type queryStudiosResultType struct
type queryTagsResultType (line 156) | type queryTagsResultType struct
type tagCategoryOutput (line 161) | type tagCategoryOutput struct
method UUID (line 167) | func (tc tagCategoryOutput) UUID() uuid.UUID {
type queryTagCategoriesResultType (line 171) | type queryTagCategoriesResultType struct
type userOutput (line 176) | type userOutput struct
method UUID (line 181) | func (u userOutput) UUID() uuid.UUID {
type draftSubmissionStatusOutput (line 185) | type draftSubmissionStatusOutput struct
method UUID (line 189) | func (d draftSubmissionStatusOutput) UUID() *uuid.UUID {
type draftEntityOutput (line 197) | type draftEntityOutput struct
type draftFingerprintOutput (line 202) | type draftFingerprintOutput struct
type sceneDraftOutput (line 208) | type sceneDraftOutput struct
type performerDraftOutput (line 222) | type performerDraftOutput struct
type draftOutput (line 244) | type draftOutput struct
method UUID (line 251) | func (d draftOutput) UUID() uuid.UUID {
type notificationOutput (line 255) | type notificationOutput struct
type queryNotificationsResultType (line 260) | type queryNotificationsResultType struct
function makeFragment (line 265) | func makeFragment(t reflect.Type) string {
type graphqlClient (line 292) | type graphqlClient struct
method createScene (line 296) | func (c *graphqlClient) createScene(input models.SceneCreateInput) (*s...
method findScene (line 314) | func (c *graphqlClient) findScene(id uuid.UUID) (*sceneOutput, error) {
method findScenesBySceneFingerprints (line 332) | func (c *graphqlClient) findScenesBySceneFingerprints(sceneFingerprint...
method queryScenes (line 350) | func (c *graphqlClient) queryScenes(input models.SceneQueryInput) (*qu...
method updateScene (line 368) | func (c *graphqlClient) updateScene(updateInput models.SceneUpdateInpu...
method destroyScene (line 386) | func (c *graphqlClient) destroyScene(input models.SceneDestroyInput) (...
method submitFingerprint (line 402) | func (c *graphqlClient) submitFingerprint(input models.FingerprintSubm...
method submitFingerprints (line 424) | func (c *graphqlClient) submitFingerprints(input []models.FingerprintB...
method sceneMoveFingerprintSubmissions (line 455) | func (c *graphqlClient) sceneMoveFingerprintSubmissions(input models.M...
method sceneDeleteFingerprintSubmissions (line 471) | func (c *graphqlClient) sceneDeleteFingerprintSubmissions(input models...
method createPerformer (line 487) | func (c *graphqlClient) createPerformer(input models.PerformerCreateIn...
method findPerformer (line 505) | func (c *graphqlClient) findPerformer(id uuid.UUID) (*performerOutput,...
method createStudio (line 523) | func (c *graphqlClient) createStudio(input models.StudioCreateInput) (...
method findStudio (line 541) | func (c *graphqlClient) findStudio(id uuid.UUID) (*studioOutput, error) {
method createTag (line 559) | func (c *graphqlClient) createTag(input models.TagCreateInput) (*tagOu...
method findSite (line 577) | func (c *graphqlClient) findSite(id uuid.UUID) (*siteOutput, error) {
method querySites (line 595) | func (c *graphqlClient) querySites() (*querySitesResultType, error) {
method updateSite (line 613) | func (c *graphqlClient) updateSite(input models.SiteUpdateInput) (*sit...
method destroySite (line 631) | func (c *graphqlClient) destroySite(input models.SiteDestroyInput) (bo...
method queryPerformers (line 647) | func (c *graphqlClient) queryPerformers(input models.PerformerQueryInp...
method queryStudios (line 665) | func (c *graphqlClient) queryStudios(input models.StudioQueryInput) (*...
method queryTags (line 683) | func (c *graphqlClient) queryTags(input models.TagQueryInput) (*queryT...
method queryTagCategories (line 701) | func (c *graphqlClient) queryTagCategories() (*queryTagCategoriesResul...
method findTagOrAlias (line 719) | func (c *graphqlClient) findTagOrAlias(name string) (*tagOutput, error) {
method me (line 737) | func (c *graphqlClient) me() (*userOutput, error) {
method favoritePerformer (line 755) | func (c *graphqlClient) favoritePerformer(id uuid.UUID, favorite bool)...
method favoriteStudio (line 771) | func (c *graphqlClient) favoriteStudio(id uuid.UUID, favorite bool) (b...
method submitSceneDraft (line 787) | func (c *graphqlClient) submitSceneDraft(input models.SceneDraftInput)...
method submitPerformerDraft (line 805) | func (c *graphqlClient) submitPerformerDraft(input models.PerformerDra...
method findDraft (line 823) | func (c *graphqlClient) findDraft(id uuid.UUID) (*draftOutput, error) {
method findDraftWithTags (line 843) | func (c *graphqlClient) findDraftWithTags(id uuid.UUID) (*draftOutput,...
method findDrafts (line 877) | func (c *graphqlClient) findDrafts() ([]draftOutput, error) {
method destroyDraft (line 897) | func (c *graphqlClient) destroyDraft(id uuid.UUID) (bool, error) {
method queryNotifications (line 913) | func (c *graphqlClient) queryNotifications(input models.QueryNotificat...
method getUnreadNotificationCount (line 935) | func (c *graphqlClient) getUnreadNotificationCount() (int, error) {
method updateNotificationSubscriptions (line 951) | func (c *graphqlClient) updateNotificationSubscriptions(subscriptions ...
method markNotificationsRead (line 967) | func (c *graphqlClient) markNotificationsRead(notification *models.Mar...
method getNotificationSubscriptions (line 983) | func (c *graphqlClient) getNotificationSubscriptions() ([]models.Notif...
method deleteEdit (line 1003) | func (c *graphqlClient) deleteEdit(input models.DeleteEditInput) (bool...
method amendEdit (line 1019) | func (c *graphqlClient) amendEdit(input models.AmendEditInput) (bool, ...
type fingerprintSubmissionResultOutput (line 418) | type fingerprintSubmissionResultOutput struct
FILE: internal/api/integration_test.go
type userPopulator (line 27) | type userPopulator struct
method PopulateDB (line 44) | func (p *userPopulator) PopulateDB(factory *service.Factory) error {
function TestMain (line 154) | func TestMain(m *testing.M) {
type testRunner (line 159) | type testRunner struct
method doTest (line 241) | func (t *testRunner) doTest(test func()) {
method fieldMismatch (line 249) | func (t *testRunner) fieldMismatch(expected interface{}, actual interf...
method updateContext (line 254) | func (t *testRunner) updateContext(fields []string) context.Context {
method generatePerformerName (line 266) | func (s *testRunner) generatePerformerName() string {
method createTestPerformer (line 271) | func (s *testRunner) createTestPerformer(input *models.PerformerCreate...
method createFullPerformerCreateInput (line 289) | func (s *testRunner) createFullPerformerCreateInput() *models.Performe...
method generateStudioName (line 352) | func (s *testRunner) generateStudioName() string {
method createTestStudio (line 357) | func (s *testRunner) createTestStudio(input *models.StudioCreateInput)...
method generateTagName (line 375) | func (s *testRunner) generateTagName() string {
method createTestTag (line 380) | func (s *testRunner) createTestTag(input *models.TagCreateInput) (*tag...
method generateSceneName (line 398) | func (s *testRunner) generateSceneName() string {
method createTestScene (line 403) | func (s *testRunner) createTestScene(input *models.SceneCreateInput) (...
method generateSceneFingerprint (line 430) | func (s *testRunner) generateSceneFingerprint(userIDs []uuid.UUID) mod...
method generateSceneFingerprintWithAlgorithm (line 434) | func (s *testRunner) generateSceneFingerprintWithAlgorithm(algorithm m...
method generateUserName (line 448) | func (s *testRunner) generateUserName() string {
method createTestUser (line 453) | func (s *testRunner) createTestUser(input *models.UserCreateInput, rol...
method generateCategoryName (line 483) | func (s *testRunner) generateCategoryName() string {
method createTestTagCategory (line 488) | func (s *testRunner) createTestTagCategory(input *models.TagCategoryCr...
method createTestTagEdit (line 511) | func (s *testRunner) createTestTagEdit(operation models.OperationEnum,...
method createTestStudioEdit (line 544) | func (s *testRunner) createTestStudioEdit(operation models.OperationEn...
method applyEdit (line 577) | func (s *testRunner) applyEdit(id uuid.UUID) (*models.Edit, error) {
method getEditTagDetails (line 593) | func (s *testRunner) getEditTagDetails(input *models.Edit) *models.Tag...
method getEditTagTarget (line 602) | func (s *testRunner) getEditTagTarget(input *models.Edit) *models.Tag {
method getEditStudioDetails (line 611) | func (s *testRunner) getEditStudioDetails(input *models.Edit) *models....
method getEditStudioTarget (line 620) | func (s *testRunner) getEditStudioTarget(input *models.Edit) *models.S...
method verifyEditOperation (line 637) | func (s *testRunner) verifyEditOperation(operation string, edit *model...
method verifyEditStatus (line 643) | func (s *testRunner) verifyEditStatus(status string, edit *models.Edit) {
method verifyEditApplication (line 649) | func (s *testRunner) verifyEditApplication(applied bool, edit *models....
method verifyEditTargetType (line 655) | func (s *testRunner) verifyEditTargetType(targetType string, edit *mod...
method verifyAppliedPerformerEdit (line 661) | func (s *testRunner) verifyAppliedPerformerEdit(edit *models.Edit) {
method verifyAppliedSceneEdit (line 668) | func (s *testRunner) verifyAppliedSceneEdit(edit *models.Edit) {
method createTestPerformerEdit (line 675) | func (s *testRunner) createTestPerformerEdit(operation models.Operatio...
method getEditPerformerDetails (line 709) | func (s *testRunner) getEditPerformerDetails(input *models.Edit) *mode...
method getEditPerformerTarget (line 718) | func (s *testRunner) getEditPerformerTarget(input *models.Edit) *model...
method createPerformerEditDetailsInput (line 727) | func (s *testRunner) createPerformerEditDetailsInput() *models.Perform...
method createFullSceneCreateInput (line 790) | func (s *testRunner) createFullSceneCreateInput() *models.SceneCreateI...
method createSceneEditDetailsInput (line 823) | func (s *testRunner) createSceneEditDetailsInput() *models.SceneEditDe...
method createFullSceneEditDetailsInput (line 853) | func (s *testRunner) createFullSceneEditDetailsInput() *models.SceneEd...
method createTestSceneEdit (line 904) | func (s *testRunner) createTestSceneEdit(operation models.OperationEnu...
method getEditSceneDetails (line 937) | func (s *testRunner) getEditSceneDetails(input *models.Edit) *models.S...
method getEditSceneTarget (line 946) | func (s *testRunner) getEditSceneTarget(input *models.Edit) *models.Sc...
method generateSiteName (line 955) | func (s *testRunner) generateSiteName() string {
method createTestSite (line 960) | func (s *testRunner) createTestSite(input *models.SiteCreateInput) (*m...
method compareSiteURLs (line 983) | func (s *testRunner) compareSiteURLs(input []models.URL, output []site...
method getUserNotificationSubscriptions (line 1115) | func (s *testRunner) getUserNotificationSubscriptions() ([]models.Noti...
function createTestRunner (line 176) | func createTestRunner(t *testing.T, u *models.User, roles []models.RoleE...
function asAdmin (line 217) | func asAdmin(t *testing.T) *testRunner {
function asModify (line 221) | func asModify(t *testing.T) *testRunner {
function asModerate (line 225) | func asModerate(t *testing.T) *testRunner {
function asRead (line 229) | func asRead(t *testing.T) *testRunner {
function asNone (line 233) | func asNone(t *testing.T) *testRunner {
function asEdit (line 237) | func asEdit(t *testing.T) *testRunner {
function oneNil (line 629) | func oneNil(l interface{}, r interface{}) bool {
function bothNil (line 633) | func bothNil(l interface{}, r interface{}) bool {
function comparePerformers (line 995) | func comparePerformers(input []models.PerformerAppearanceInput, performe...
function comparePerformersInput (line 1020) | func comparePerformersInput(input, performers []models.PerformerAppearan...
function compareTags (line 1045) | func compareTags(tagIDs []uuid.UUID, tags []idObject) bool {
function compareFingerprints (line 1060) | func compareFingerprints(input []models.FingerprintEditInput, fingerprin...
function compareFingerprintsInput (line 1074) | func compareFingerprintsInput(input, fingerprints []models.FingerprintEd...
function assertBodyMods (line 1088) | func assertBodyMods(t *testing.T, input []models.BodyModificationInput, ...
FILE: internal/api/loaders.go
function tagList (line 11) | func tagList(ctx context.Context, tagIDs []uuid.UUID) ([]models.Tag, err...
function imageList (line 33) | func imageList(ctx context.Context, imageIDs []uuid.UUID) ([]models.Imag...
FILE: internal/api/notification_integration_test.go
type notificationTestRunner (line 15) | type notificationTestRunner struct
method testNotificationOnCommentOwnEdit (line 26) | func (s *notificationTestRunner) testNotificationOnCommentOwnEdit() {
method testNotificationOnDownvoteOwnEdit (line 83) | func (s *notificationTestRunner) testNotificationOnDownvoteOwnEdit() {
method testNotificationOnFailedOwnEdit (line 129) | func (s *notificationTestRunner) testNotificationOnFailedOwnEdit() {
method testNotificationOnAdminCancelEdit (line 161) | func (s *notificationTestRunner) testNotificationOnAdminCancelEdit() {
method testMarkSpecificNotificationRead (line 204) | func (s *notificationTestRunner) testMarkSpecificNotificationRead() {
method testMarkAllNotificationsRead (line 271) | func (s *notificationTestRunner) testMarkAllNotificationsRead() {
method testNotificationSubscriptionRoleEnforcement (line 362) | func (s *notificationTestRunner) testNotificationSubscriptionRoleEnfor...
method testQueryNotificationsPagination (line 436) | func (s *notificationTestRunner) testQueryNotificationsPagination() {
method testQueryNotificationsTypeFilter (line 527) | func (s *notificationTestRunner) testQueryNotificationsTypeFilter() {
method testNotificationOnFavoriteStudioScene (line 615) | func (s *notificationTestRunner) testNotificationOnFavoriteStudioScene...
function createNotificationTestRunner (line 19) | func createNotificationTestRunner(t *testing.T) *notificationTestRunner {
function pointerTo (line 327) | func pointerTo[T any](v T) *T {
function TestNotificationOnCommentOwnEdit (line 331) | func TestNotificationOnCommentOwnEdit(t *testing.T) {
function TestNotificationOnDownvoteOwnEdit (line 336) | func TestNotificationOnDownvoteOwnEdit(t *testing.T) {
function TestNotificationOnFailedOwnEdit (line 341) | func TestNotificationOnFailedOwnEdit(t *testing.T) {
function TestNotificationOnAdminCancelEdit (line 346) | func TestNotificationOnAdminCancelEdit(t *testing.T) {
function TestMarkSpecificNotificationRead (line 351) | func TestMarkSpecificNotificationRead(t *testing.T) {
function TestMarkAllNotificationsRead (line 356) | func TestMarkAllNotificationsRead(t *testing.T) {
function TestNotificationSubscriptionRoleEnforcement (line 430) | func TestNotificationSubscriptionRoleEnforcement(t *testing.T) {
function TestQueryNotificationsPagination (line 521) | func TestQueryNotificationsPagination(t *testing.T) {
function TestQueryNotificationsTypeFilter (line 608) | func TestQueryNotificationsTypeFilter(t *testing.T) {
function TestNotificationOnFavoriteStudioScene (line 684) | func TestNotificationOnFavoriteStudioScene(t *testing.T) {
FILE: internal/api/performer_edit_integration_test.go
type performerEditTestRunner (line 14) | type performerEditTestRunner struct
method compareBodyModifications (line 33) | func (s *performerEditTestRunner) compareBodyModifications(input []mod...
method testCreatePerformerEdit (line 45) | func (s *performerEditTestRunner) testCreatePerformerEdit() {
method verifyCreatedPerformerEdit (line 52) | func (s *performerEditTestRunner) verifyCreatedPerformerEdit(input mod...
method testFindEditById (line 63) | func (s *performerEditTestRunner) testFindEditById() {
method testModifyPerformerEdit (line 73) | func (s *performerEditTestRunner) testModifyPerformerEdit() {
method verifyUpdatedPerformerEdit (line 99) | func (s *performerEditTestRunner) verifyUpdatedPerformerEdit(originalP...
method verifyPerformerEditDetails (line 108) | func (s *performerEditTestRunner) verifyPerformerEditDetails(input mod...
method verifyPerformerEdit (line 203) | func (s *performerEditTestRunner) verifyPerformerEdit(input models.Per...
method testDestroyPerformerEdit (line 336) | func (s *performerEditTestRunner) testDestroyPerformerEdit() {
method verifyDestroyPerformerEdit (line 353) | func (s *performerEditTestRunner) verifyDestroyPerformerEdit(performer...
method testMergePerformerEdit (line 364) | func (s *performerEditTestRunner) testMergePerformerEdit() {
method verifyMergePerformerEdit (line 392) | func (s *performerEditTestRunner) verifyMergePerformerEdit(originalPer...
method testApplyCreatePerformerEdit (line 409) | func (s *performerEditTestRunner) testApplyCreatePerformerEdit() {
method verifyAppliedPerformerCreateEdit (line 419) | func (s *performerEditTestRunner) verifyAppliedPerformerCreateEdit(inp...
method testApplyModifyPerformerEdit (line 431) | func (s *performerEditTestRunner) testApplyModifyPerformerEdit() {
method testApplyModifyPerformerWithoutAliases (line 478) | func (s *performerEditTestRunner) testApplyModifyPerformerWithoutAlias...
method testApplyModifyPerformerWithAliases (line 540) | func (s *performerEditTestRunner) testApplyModifyPerformerWithAliases() {
method testApplyModifyUnsetPerformerEdit (line 581) | func (s *performerEditTestRunner) testApplyModifyUnsetPerformerEdit() {
method testApplyDestroyPerformerEdit (line 647) | func (s *performerEditTestRunner) testApplyDestroyPerformerEdit() {
method verifyApplyDestroyPerformerEdit (line 681) | func (s *performerEditTestRunner) verifyApplyDestroyPerformerEdit(dest...
method testApplyMergePerformerEdit (line 693) | func (s *performerEditTestRunner) testApplyMergePerformerEdit() {
method verifyAppliedMergePerformerEdit (line 794) | func (s *performerEditTestRunner) verifyAppliedMergePerformerEdit(inpu...
method verifyPerformanceAlias (line 818) | func (s *performerEditTestRunner) verifyPerformanceAlias(scene *sceneO...
method testApplyMergePerformerEditWithoutAlias (line 830) | func (s *performerEditTestRunner) testApplyMergePerformerEditWithoutAl...
method testPerformerEditUpdate (line 915) | func (s *performerEditTestRunner) testPerformerEditUpdate() {
method testQueryExistingPerformer (line 1051) | func (s *performerEditTestRunner) testQueryExistingPerformer() {
method testApplyModifyPerformerEnumFields (line 1256) | func (s *performerEditTestRunner) testApplyModifyPerformerEnumFields() {
function contains (line 18) | func contains(slice []uuid.UUID, item uuid.UUID) bool {
function createPerformerEditTestRunner (line 27) | func createPerformerEditTestRunner(t *testing.T) *performerEditTestRunner {
method testChangeURLSite (line 871) | func (s *performerTestRunner) testChangeURLSite() {
function TestCreatePerformerEdit (line 985) | func TestCreatePerformerEdit(t *testing.T) {
function TestModifyPerformerEdit (line 990) | func TestModifyPerformerEdit(t *testing.T) {
function TestDestroyPerformerEdit (line 995) | func TestDestroyPerformerEdit(t *testing.T) {
function TestMergePerformerEdit (line 1000) | func TestMergePerformerEdit(t *testing.T) {
function TestApplyCreatePerformerEdit (line 1005) | func TestApplyCreatePerformerEdit(t *testing.T) {
function TestApplyModifyPerformerEdit (line 1010) | func TestApplyModifyPerformerEdit(t *testing.T) {
function TestApplyModifyPerformerEditOptions (line 1015) | func TestApplyModifyPerformerEditOptions(t *testing.T) {
function TestApplyMergePerformerEditWithoutAlias (line 1021) | func TestApplyMergePerformerEditWithoutAlias(t *testing.T) {
function TestApplyModifyUnsetPerformerEdit (line 1026) | func TestApplyModifyUnsetPerformerEdit(t *testing.T) {
function TestApplyDestroyPerformerEdit (line 1031) | func TestApplyDestroyPerformerEdit(t *testing.T) {
function TestApplyMergePerformerEdit (line 1036) | func TestApplyMergePerformerEdit(t *testing.T) {
function TestChangeURLSite (line 1041) | func TestChangeURLSite(t *testing.T) {
function TestPerformerEditUpdate (line 1046) | func TestPerformerEditUpdate(t *testing.T) {
function TestQueryExistingPerformer (line 1251) | func TestQueryExistingPerformer(t *testing.T) {
function TestApplyModifyPerformerEnumFields (line 1311) | func TestApplyModifyPerformerEnumFields(t *testing.T) {
FILE: internal/api/performer_integration_test.go
type performerTestRunner (line 14) | type performerTestRunner struct
method testCreatePerformer (line 24) | func (s *performerTestRunner) testCreatePerformer() {
method verifyCreatedPerformer (line 87) | func (s *performerTestRunner) verifyCreatedPerformer(input models.Perf...
method testFindPerformer (line 145) | func (s *performerTestRunner) testFindPerformer() {
method testUpdatePerformer (line 158) | func (s *performerTestRunner) testUpdatePerformer() {
method verifyUpdatedPerformer (line 250) | func (s *performerTestRunner) verifyUpdatedPerformer(input models.Perf...
method testDestroyPerformer (line 287) | func (s *performerTestRunner) testDestroyPerformer() {
method testQueryPerformers (line 308) | func (s *performerTestRunner) testQueryPerformers() {
method testQueryPerformersBirthdate (line 353) | func (s *performerTestRunner) testQueryPerformersBirthdate() {
method testQueryPerformersByAgeAndBirthYear (line 513) | func (s *performerTestRunner) testQueryPerformersByAgeAndBirthYear() {
method testQueryPerformersSceneCountSort (line 673) | func (s *performerTestRunner) testQueryPerformersSceneCountSort() {
function createPerformerTestRunner (line 18) | func createPerformerTestRunner(t *testing.T) *performerTestRunner {
function TestCreatePerformer (line 490) | func TestCreatePerformer(t *testing.T) {
function TestFindPerformer (line 495) | func TestFindPerformer(t *testing.T) {
function TestUpdatePerformer (line 500) | func TestUpdatePerformer(t *testing.T) {
function TestDestroyPerformer (line 508) | func TestDestroyPerformer(t *testing.T) {
function TestQueryPerformers (line 816) | func TestQueryPerformers(t *testing.T) {
function TestQueryPerformersBirthdate (line 821) | func TestQueryPerformersBirthdate(t *testing.T) {
function TestQueryPerformersByAgeAndBirthYear (line 826) | func TestQueryPerformersByAgeAndBirthYear(t *testing.T) {
function TestQueryPerformersSceneCountSort (line 831) | func TestQueryPerformersSceneCountSort(t *testing.T) {
FILE: internal/api/performer_resolver_integration_test.go
type performerResolverTestRunner (line 13) | type performerResolverTestRunner struct
method testPerformerImages (line 24) | func (s *performerResolverTestRunner) testPerformerImages() {
method testPerformerDeleted (line 39) | func (s *performerResolverTestRunner) testPerformerDeleted() {
method testPerformerEdits (line 67) | func (s *performerResolverTestRunner) testPerformerEdits() {
method testPerformerSceneCount (line 102) | func (s *performerResolverTestRunner) testPerformerSceneCount() {
method testPerformerScenes (line 139) | func (s *performerResolverTestRunner) testPerformerScenes() {
method testPerformerStudios (line 192) | func (s *performerResolverTestRunner) testPerformerStudios() {
method testPerformerCreatedUpdated (line 264) | func (s *performerResolverTestRunner) testPerformerCreatedUpdated() {
method testPerformerAge (line 303) | func (s *performerResolverTestRunner) testPerformerAge() {
method testPerformerAliases (line 354) | func (s *performerResolverTestRunner) testPerformerAliases() {
method testPerformerUrls (line 383) | func (s *performerResolverTestRunner) testPerformerUrls() {
method testPerformerBirthdate (line 416) | func (s *performerResolverTestRunner) testPerformerBirthdate() {
method testPerformerTattoosAndPiercings (line 446) | func (s *performerResolverTestRunner) testPerformerTattoosAndPiercings...
function createPerformerResolverTestRunner (line 17) | func createPerformerResolverTestRunner(t *testing.T) *performerResolverT...
function TestPerformerImages (line 480) | func TestPerformerImages(t *testing.T) {
function TestPerformerDeleted (line 485) | func TestPerformerDeleted(t *testing.T) {
function TestPerformerEdits (line 490) | func TestPerformerEdits(t *testing.T) {
function TestPerformerSceneCount (line 495) | func TestPerformerSceneCount(t *testing.T) {
function TestPerformerScenes (line 500) | func TestPerformerScenes(t *testing.T) {
function TestPerformerStudios (line 505) | func TestPerformerStudios(t *testing.T) {
function TestPerformerCreatedUpdated (line 510) | func TestPerformerCreatedUpdated(t *testing.T) {
function TestPerformerAge (line 515) | func TestPerformerAge(t *testing.T) {
function TestPerformerAliases (line 520) | func TestPerformerAliases(t *testing.T) {
function TestPerformerUrls (line 525) | func TestPerformerUrls(t *testing.T) {
function TestPerformerBirthdate (line 530) | func TestPerformerBirthdate(t *testing.T) {
function TestPerformerTattoosAndPiercings (line 535) | func TestPerformerTattoosAndPiercings(t *testing.T) {
FILE: internal/api/resolver.go
type Resolver (line 11) | type Resolver struct
method Mutation (line 21) | func (r *Resolver) Mutation() models.MutationResolver {
method Edit (line 24) | func (r *Resolver) Edit() models.EditResolver {
method EditComment (line 27) | func (r *Resolver) EditComment() models.EditCommentResolver {
method EditVote (line 30) | func (r *Resolver) EditVote() models.EditVoteResolver {
method Performer (line 33) | func (r *Resolver) Performer() models.PerformerResolver {
method PerformerEdit (line 36) | func (r *Resolver) PerformerEdit() models.PerformerEditResolver {
method StudioEdit (line 39) | func (r *Resolver) StudioEdit() models.StudioEditResolver {
method TagEdit (line 42) | func (r *Resolver) TagEdit() models.TagEditResolver {
method SceneEdit (line 45) | func (r *Resolver) SceneEdit() models.SceneEditResolver {
method Tag (line 48) | func (r *Resolver) Tag() models.TagResolver {
method TagCategory (line 51) | func (r *Resolver) TagCategory() models.TagCategoryResolver {
method Image (line 54) | func (r *Resolver) Image() models.ImageResolver {
method Studio (line 57) | func (r *Resolver) Studio() models.StudioResolver {
method Scene (line 60) | func (r *Resolver) Scene() models.SceneResolver {
method Site (line 63) | func (r *Resolver) Site() models.SiteResolver {
method URL (line 66) | func (r *Resolver) URL() models.URLResolver {
method User (line 69) | func (r *Resolver) User() models.UserResolver {
method Query (line 72) | func (r *Resolver) Query() models.QueryResolver {
method QueryPerformersResultType (line 75) | func (r *Resolver) QueryPerformersResultType() models.QueryPerformersR...
method QueryScenesResultType (line 78) | func (r *Resolver) QueryScenesResultType() models.QueryScenesResultTyp...
method QueryEditsResultType (line 81) | func (r *Resolver) QueryEditsResultType() models.QueryEditsResultTypeR...
method Draft (line 84) | func (r *Resolver) Draft() models.DraftResolver {
method PerformerDraft (line 87) | func (r *Resolver) PerformerDraft() models.PerformerDraftResolver {
method SceneDraft (line 90) | func (r *Resolver) SceneDraft() models.SceneDraftResolver {
method QueryExistingSceneResult (line 93) | func (r *Resolver) QueryExistingSceneResult() models.QueryExistingScen...
method QueryExistingPerformerResult (line 96) | func (r *Resolver) QueryExistingPerformerResult() models.QueryExisting...
method QueryNotificationsResult (line 99) | func (r *Resolver) QueryNotificationsResult() models.QueryNotification...
method Notification (line 102) | func (r *Resolver) Notification() models.NotificationResolver {
method QueryModAuditsResultType (line 105) | func (r *Resolver) QueryModAuditsResultType() models.QueryModAuditsRes...
method ModAudit (line 108) | func (r *Resolver) ModAudit() models.ModAuditResolver {
function NewResolver (line 15) | func NewResolver(fac service.Factory) *Resolver {
type mutationResolver (line 112) | type mutationResolver struct
type queryResolver (line 114) | type queryResolver struct
method Version (line 116) | func (r *queryResolver) Version(ctx context.Context) (*models.Version,...
method GetConfig (line 127) | func (r *queryResolver) GetConfig(ctx context.Context) (*models.StashB...
FILE: internal/api/resolver_model_draft.go
type draftResolver (line 12) | type draftResolver struct
method Created (line 14) | func (r *draftResolver) Created(ctx context.Context, obj *models.Draft...
method Expires (line 18) | func (r *draftResolver) Expires(ctx context.Context, obj *models.Draft...
method Data (line 24) | func (r *draftResolver) Data(ctx context.Context, obj *models.Draft) (...
FILE: internal/api/resolver_model_edit.go
type editResolver (line 13) | type editResolver struct
method ID (line 15) | func (r *editResolver) ID(ctx context.Context, obj *models.Edit) (stri...
method User (line 19) | func (r *editResolver) User(ctx context.Context, obj *models.Edit) (*m...
method Created (line 27) | func (r *editResolver) Created(ctx context.Context, obj *models.Edit) ...
method Updated (line 31) | func (r *editResolver) Updated(ctx context.Context, obj *models.Edit) ...
method Closed (line 35) | func (r *editResolver) Closed(ctx context.Context, obj *models.Edit) (...
method Expires (line 39) | func (r *editResolver) Expires(ctx context.Context, obj *models.Edit) ...
method Target (line 62) | func (r *editResolver) Target(ctx context.Context, obj *models.Edit) (...
method TargetType (line 74) | func (r *editResolver) TargetType(ctx context.Context, obj *models.Edi...
method MergeSources (line 83) | func (r *editResolver) MergeSources(ctx context.Context, obj *models.E...
method Operation (line 96) | func (r *editResolver) Operation(ctx context.Context, obj *models.Edit...
method Details (line 105) | func (r *editResolver) Details(ctx context.Context, obj *models.Edit) ...
method OldDetails (line 152) | func (r *editResolver) OldDetails(ctx context.Context, obj *models.Edi...
method Comments (line 187) | func (r *editResolver) Comments(ctx context.Context, obj *models.Edit)...
method Votes (line 191) | func (r *editResolver) Votes(ctx context.Context, obj *models.Edit) ([...
method Status (line 195) | func (r *editResolver) Status(ctx context.Context, obj *models.Edit) (...
method Options (line 204) | func (r *editResolver) Options(ctx context.Context, obj *models.Edit) ...
method Destructive (line 220) | func (r *editResolver) Destructive(ctx context.Context, obj *models.Ed...
method Updatable (line 224) | func (r *editResolver) Updatable(ctx context.Context, obj *models.Edit...
FILE: internal/api/resolver_model_edit_comment.go
type editCommentResolver (line 10) | type editCommentResolver struct
method ID (line 12) | func (r *editCommentResolver) ID(ctx context.Context, obj *models.Edit...
method Comment (line 16) | func (r *editCommentResolver) Comment(ctx context.Context, obj *models...
method Date (line 20) | func (r *editCommentResolver) Date(ctx context.Context, obj *models.Ed...
method User (line 24) | func (r *editCommentResolver) User(ctx context.Context, obj *models.Ed...
method Edit (line 32) | func (r *editCommentResolver) Edit(ctx context.Context, obj *models.Ed...
FILE: internal/api/resolver_model_edit_vote.go
type editVoteResolver (line 12) | type editVoteResolver struct
method Vote (line 14) | func (r *editVoteResolver) Vote(ctx context.Context, obj *models.EditV...
method Date (line 22) | func (r *editVoteResolver) Date(ctx context.Context, obj *models.EditV...
method User (line 26) | func (r *editVoteResolver) User(ctx context.Context, obj *models.EditV...
FILE: internal/api/resolver_model_image.go
type imageResolver (line 9) | type imageResolver struct
method ID (line 11) | func (r *imageResolver) ID(ctx context.Context, obj *models.Image) (st...
method URL (line 14) | func (r *imageResolver) URL(ctx context.Context, obj *models.Image) (s...
FILE: internal/api/resolver_model_notification.go
type notificationResolver (line 11) | type notificationResolver struct
method Created (line 13) | func (r *notificationResolver) Created(ctx context.Context, obj *model...
method Read (line 17) | func (r *notificationResolver) Read(ctx context.Context, obj *models.N...
method Data (line 21) | func (r *notificationResolver) Data(ctx context.Context, obj *models.N...
FILE: internal/api/resolver_model_performer.go
type performerResolver (line 16) | type performerResolver struct
method ID (line 18) | func (r *performerResolver) ID(ctx context.Context, obj *models.Perfor...
method Aliases (line 22) | func (r *performerResolver) Aliases(ctx context.Context, obj *models.P...
method Urls (line 33) | func (r *performerResolver) Urls(ctx context.Context, obj *models.Perf...
method Birthdate (line 38) | func (r *performerResolver) Birthdate(ctx context.Context, obj *models...
method Age (line 42) | func (r *performerResolver) Age(ctx context.Context, obj *models.Perfo...
method Measurements (line 71) | func (r *performerResolver) Measurements(ctx context.Context, obj *mod...
method Tattoos (line 81) | func (r *performerResolver) Tattoos(ctx context.Context, obj *models.P...
method Piercings (line 85) | func (r *performerResolver) Piercings(ctx context.Context, obj *models...
method Images (line 89) | func (r *performerResolver) Images(ctx context.Context, obj *models.Pe...
method Edits (line 99) | func (r *performerResolver) Edits(ctx context.Context, obj *models.Per...
method SceneCount (line 103) | func (r *performerResolver) SceneCount(ctx context.Context, obj *model...
method Scenes (line 107) | func (r *performerResolver) Scenes(ctx context.Context, obj *models.Pe...
method MergedIds (line 144) | func (r *performerResolver) MergedIds(ctx context.Context, obj *models...
method MergedIntoID (line 148) | func (r *performerResolver) MergedIntoID(ctx context.Context, obj *mod...
method Studios (line 158) | func (r *performerResolver) Studios(ctx context.Context, obj *models.P...
method IsFavorite (line 162) | func (r *performerResolver) IsFavorite(ctx context.Context, obj *model...
FILE: internal/api/resolver_model_performer_draft.go
type performerDraftResolver (line 9) | type performerDraftResolver struct
method ID (line 11) | func (r *performerDraftResolver) ID(ctx context.Context, obj *models.P...
method Image (line 19) | func (r *performerDraftResolver) Image(ctx context.Context, obj *model...
FILE: internal/api/resolver_model_performer_edit.go
type performerEditResolver (line 10) | type performerEditResolver struct
method Gender (line 12) | func (r *performerEditResolver) Gender(ctx context.Context, obj *model...
method HairColor (line 21) | func (r *performerEditResolver) HairColor(ctx context.Context, obj *mo...
method EyeColor (line 30) | func (r *performerEditResolver) EyeColor(ctx context.Context, obj *mod...
method Ethnicity (line 39) | func (r *performerEditResolver) Ethnicity(ctx context.Context, obj *mo...
method BreastType (line 48) | func (r *performerEditResolver) BreastType(ctx context.Context, obj *m...
method AddedImages (line 57) | func (r *performerEditResolver) AddedImages(ctx context.Context, obj *...
method RemovedImages (line 61) | func (r *performerEditResolver) RemovedImages(ctx context.Context, obj...
method Images (line 65) | func (r *performerEditResolver) Images(ctx context.Context, obj *model...
method Urls (line 69) | func (r *performerEditResolver) Urls(ctx context.Context, obj *models....
method Aliases (line 73) | func (r *performerEditResolver) Aliases(ctx context.Context, obj *mode...
method Tattoos (line 77) | func (r *performerEditResolver) Tattoos(ctx context.Context, obj *mode...
method Piercings (line 81) | func (r *performerEditResolver) Piercings(ctx context.Context, obj *mo...
FILE: internal/api/resolver_model_scene.go
type sceneResolver (line 12) | type sceneResolver struct
method ID (line 14) | func (r *sceneResolver) ID(ctx context.Context, obj *models.Scene) (st...
method Date (line 19) | func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (...
method ReleaseDate (line 27) | func (r *sceneResolver) ReleaseDate(ctx context.Context, obj *models.S...
method Studio (line 31) | func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene)...
method Tags (line 44) | func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (...
method Images (line 52) | func (r *sceneResolver) Images(ctx context.Context, obj *models.Scene)...
method Performers (line 63) | func (r *sceneResolver) Performers(ctx context.Context, obj *models.Sc...
method Fingerprints (line 86) | func (r *sceneResolver) Fingerprints(ctx context.Context, obj *models....
method Urls (line 93) | func (r *sceneResolver) Urls(ctx context.Context, obj *models.Scene) (...
method Edits (line 97) | func (r *sceneResolver) Edits(ctx context.Context, obj *models.Scene) ...
method Created (line 101) | func (r *sceneResolver) Created(ctx context.Context, obj *models.Scene...
method Updated (line 105) | func (r *sceneResolver) Updated(ctx context.Context, obj *models.Scene...
FILE: internal/api/resolver_model_scene_draft.go
type sceneDraftResolver (line 9) | type sceneDraftResolver struct
method ID (line 11) | func (r *sceneDraftResolver) ID(ctx context.Context, obj *models.Scene...
method Image (line 19) | func (r *sceneDraftResolver) Image(ctx context.Context, obj *models.Sc...
method Performers (line 27) | func (r *sceneDraftResolver) Performers(ctx context.Context, obj *mode...
method Tags (line 31) | func (r *sceneDraftResolver) Tags(ctx context.Context, obj *models.Sce...
method Studio (line 35) | func (r *sceneDraftResolver) Studio(ctx context.Context, obj *models.S...
FILE: internal/api/resolver_model_scene_edit.go
type sceneEditResolver (line 11) | type sceneEditResolver struct
method Studio (line 13) | func (r *sceneEditResolver) Studio(ctx context.Context, obj *models.Sc...
method performerAppearanceList (line 21) | func (r *sceneEditResolver) performerAppearanceList(ctx context.Contex...
method AddedPerformers (line 49) | func (r *sceneEditResolver) AddedPerformers(ctx context.Context, obj *...
method RemovedPerformers (line 53) | func (r *sceneEditResolver) RemovedPerformers(ctx context.Context, obj...
method AddedTags (line 57) | func (r *sceneEditResolver) AddedTags(ctx context.Context, obj *models...
method RemovedTags (line 61) | func (r *sceneEditResolver) RemovedTags(ctx context.Context, obj *mode...
method AddedImages (line 65) | func (r *sceneEditResolver) AddedImages(ctx context.Context, obj *mode...
method RemovedImages (line 69) | func (r *sceneEditResolver) RemovedImages(ctx context.Context, obj *mo...
method fingerprintList (line 73) | func (r *sceneEditResolver) fingerprintList(ctx context.Context, finge...
method AddedFingerprints (line 87) | func (r *sceneEditResolver) AddedFingerprints(ctx context.Context, obj...
method RemovedFingerprints (line 91) | func (r *sceneEditResolver) RemovedFingerprints(ctx context.Context, o...
method Fingerprints (line 95) | func (r *sceneEditResolver) Fingerprints(ctx context.Context, obj *mod...
method Images (line 110) | func (r *sceneEditResolver) Images(ctx context.Context, obj *models.Sc...
method Tags (line 114) | func (r *sceneEditResolver) Tags(ctx context.Context, obj *models.Scen...
method Performers (line 118) | func (r *sceneEditResolver) Performers(ctx context.Context, obj *model...
method Urls (line 122) | func (r *sceneEditResolver) Urls(ctx context.Context, obj *models.Scen...
FILE: internal/api/resolver_model_site.go
type siteResolver (line 11) | type siteResolver struct
method ValidTypes (line 13) | func (r *siteResolver) ValidTypes(ctx context.Context, obj *models.Sit...
method Created (line 25) | func (r *siteResolver) Created(ctx context.Context, obj *models.Site) ...
method Updated (line 29) | func (r *siteResolver) Updated(ctx context.Context, obj *models.Site) ...
method Icon (line 33) | func (r *siteResolver) Icon(ctx context.Context, obj *models.Site) (st...
FILE: internal/api/resolver_model_studio.go
type studioResolver (line 13) | type studioResolver struct
method ID (line 15) | func (r *studioResolver) ID(ctx context.Context, obj *models.Studio) (...
method Urls (line 19) | func (r *studioResolver) Urls(ctx context.Context, obj *models.Studio)...
method Parent (line 23) | func (r *studioResolver) Parent(ctx context.Context, obj *models.Studi...
method ChildStudios (line 31) | func (r *studioResolver) ChildStudios(ctx context.Context, obj *models...
method SubStudios (line 35) | func (r *studioResolver) SubStudios(ctx context.Context, obj *models.S...
method Images (line 47) | func (r *studioResolver) Images(ctx context.Context, obj *models.Studi...
method IsFavorite (line 55) | func (r *studioResolver) IsFavorite(ctx context.Context, obj *models.S...
method Created (line 59) | func (r *studioResolver) Created(ctx context.Context, obj *models.Stud...
method Updated (line 63) | func (r *studioResolver) Updated(ctx context.Context, obj *models.Stud...
method Performers (line 67) | func (r *studioResolver) Performers(ctx context.Context, obj *models.S...
method Aliases (line 74) | func (r *studioResolver) Aliases(ctx context.Context, obj *models.Stud...
FILE: internal/api/resolver_model_studio_edit.go
type studioEditResolver (line 9) | type studioEditResolver struct
method Parent (line 11) | func (r *studioEditResolver) Parent(ctx context.Context, obj *models.S...
method AddedImages (line 19) | func (r *studioEditResolver) AddedImages(ctx context.Context, obj *mod...
method RemovedImages (line 23) | func (r *studioEditResolver) RemovedImages(ctx context.Context, obj *m...
method Images (line 27) | func (r *studioEditResolver) Images(ctx context.Context, obj *models.S...
method Urls (line 31) | func (r *studioEditResolver) Urls(ctx context.Context, obj *models.Stu...
FILE: internal/api/resolver_model_tag.go
type tagResolver (line 12) | type tagResolver struct
method ID (line 14) | func (r *tagResolver) ID(ctx context.Context, obj *models.Tag) (string...
method Description (line 17) | func (r *tagResolver) Description(ctx context.Context, obj *models.Tag...
method Aliases (line 20) | func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) ([...
method Edits (line 32) | func (r *tagResolver) Edits(ctx context.Context, obj *models.Tag) ([]m...
method Category (line 36) | func (r *tagResolver) Category(ctx context.Context, obj *models.Tag) (...
method Created (line 43) | func (r *tagResolver) Created(ctx context.Context, obj *models.Tag) (*...
method Updated (line 47) | func (r *tagResolver) Updated(ctx context.Context, obj *models.Tag) (*...
FILE: internal/api/resolver_model_tag_category.go
type tagCategoryResolver (line 10) | type tagCategoryResolver struct
method ID (line 12) | func (r *tagCategoryResolver) ID(ctx context.Context, obj *models.TagC...
method Name (line 16) | func (r *tagCategoryResolver) Name(ctx context.Context, obj *models.Ta...
method Group (line 20) | func (r *tagCategoryResolver) Group(ctx context.Context, obj *models.T...
FILE: internal/api/resolver_model_tag_edit.go
type tagEditResolver (line 9) | type tagEditResolver struct
method Category (line 11) | func (r *tagEditResolver) Category(ctx context.Context, obj *models.Ta...
method Aliases (line 19) | func (r *tagEditResolver) Aliases(ctx context.Context, obj *models.Tag...
FILE: internal/api/resolver_model_url.go
type urlResolver (line 11) | type urlResolver struct
method URL (line 13) | func (r *urlResolver) URL(ctx context.Context, obj *models.URL) (strin...
method Site (line 17) | func (r *urlResolver) Site(ctx context.Context, obj *models.URL) (*mod...
method Type (line 21) | func (r *urlResolver) Type(ctx context.Context, obj *models.URL) (stri...
FILE: internal/api/resolver_model_user.go
type userResolver (line 10) | type userResolver struct
method ID (line 12) | func (r *userResolver) ID(ctx context.Context, user *models.User) (str...
method Roles (line 16) | func (r *userResolver) Roles(ctx context.Context, user *models.User) (...
method VoteCount (line 27) | func (r *userResolver) VoteCount(ctx context.Context, obj *models.User...
method EditCount (line 31) | func (r *userResolver) EditCount(ctx context.Context, obj *models.User...
method InvitedBy (line 35) | func (r *userResolver) InvitedBy(ctx context.Context, user *models.Use...
method ActiveInviteCodes (line 43) | func (r *userResolver) ActiveInviteCodes(ctx context.Context, user *mo...
method InviteCodes (line 65) | func (r *userResolver) InviteCodes(ctx context.Context, user *models.U...
method NotificationSubscriptions (line 78) | func (r *userResolver) NotificationSubscriptions(ctx context.Context, ...
FILE: internal/api/resolver_mutation_draft.go
method SubmitSceneDraft (line 12) | func (r *mutationResolver) SubmitSceneDraft(ctx context.Context, input m...
method SubmitPerformerDraft (line 22) | func (r *mutationResolver) SubmitPerformerDraft(ctx context.Context, inp...
method DestroyDraft (line 30) | func (r *mutationResolver) DestroyDraft(ctx context.Context, id uuid.UUI...
method createImage (line 34) | func (r *mutationResolver) createImage(ctx context.Context, file *graphq...
FILE: internal/api/resolver_mutation_edit.go
method SceneEdit (line 11) | func (r *mutationResolver) SceneEdit(ctx context.Context, input models.S...
method SceneEditUpdate (line 23) | func (r *mutationResolver) SceneEditUpdate(ctx context.Context, id uuid....
method StudioEdit (line 35) | func (r *mutationResolver) StudioEdit(ctx context.Context, input models....
method StudioEditUpdate (line 43) | func (r *mutationResolver) StudioEditUpdate(ctx context.Context, id uuid...
method TagEdit (line 51) | func (r *mutationResolver) TagEdit(ctx context.Context, input models.Tag...
method TagEditUpdate (line 59) | func (r *mutationResolver) TagEditUpdate(ctx context.Context, id uuid.UU...
method PerformerEdit (line 67) | func (r *mutationResolver) PerformerEdit(ctx context.Context, input mode...
method PerformerEditUpdate (line 75) | func (r *mutationResolver) PerformerEditUpdate(ctx context.Context, id u...
method EditVote (line 83) | func (r *mutationResolver) EditVote(ctx context.Context, input models.Ed...
method EditComment (line 98) | func (r *mutationResolver) EditComment(ctx context.Context, input models...
method CancelEdit (line 106) | func (r *mutationResolver) CancelEdit(ctx context.Context, input models....
method ApplyEdit (line 115) | func (r *mutationResolver) ApplyEdit(ctx context.Context, input models.A...
method DeleteEdit (line 124) | func (r *mutationResolver) DeleteEdit(ctx context.Context, input models....
method AmendEdit (line 129) | func (r *mutationResolver) AmendEdit(ctx context.Context, input models.A...
FILE: internal/api/resolver_mutation_image.go
method ImageCreate (line 9) | func (r *mutationResolver) ImageCreate(ctx context.Context, input models...
method ImageDestroy (line 13) | func (r *mutationResolver) ImageDestroy(ctx context.Context, input model...
FILE: internal/api/resolver_mutation_notifications.go
method MarkNotificationsRead (line 10) | func (r *mutationResolver) MarkNotificationsRead(ctx context.Context, no...
method UpdateNotificationSubscriptions (line 21) | func (r *mutationResolver) UpdateNotificationSubscriptions(ctx context.C...
FILE: internal/api/resolver_mutation_performer.go
method PerformerCreate (line 12) | func (r *mutationResolver) PerformerCreate(ctx context.Context, input mo...
method PerformerUpdate (line 16) | func (r *mutationResolver) PerformerUpdate(ctx context.Context, input mo...
method PerformerDestroy (line 20) | func (r *mutationResolver) PerformerDestroy(ctx context.Context, input m...
method FavoritePerformer (line 29) | func (r *mutationResolver) FavoritePerformer(ctx context.Context, id uui...
FILE: internal/api/resolver_mutation_scene.go
method SceneCreate (line 10) | func (r *mutationResolver) SceneCreate(ctx context.Context, input models...
method SceneUpdate (line 17) | func (r *mutationResolver) SceneUpdate(ctx context.Context, input models...
method SceneDestroy (line 24) | func (r *mutationResolver) SceneDestroy(ctx context.Context, input model...
method SubmitFingerprint (line 30) | func (r *mutationResolver) SubmitFingerprint(ctx context.Context, input ...
method SubmitFingerprints (line 40) | func (r *mutationResolver) SubmitFingerprints(ctx context.Context, input...
method SceneMoveFingerprintSubmissions (line 50) | func (r *mutationResolver) SceneMoveFingerprintSubmissions(ctx context.C...
method SceneDeleteFingerprintSubmissions (line 56) | func (r *mutationResolver) SceneDeleteFingerprintSubmissions(ctx context...
FILE: internal/api/resolver_mutation_site.go
method SiteCreate (line 9) | func (r *mutationResolver) SiteCreate(ctx context.Context, input models....
method SiteUpdate (line 13) | func (r *mutationResolver) SiteUpdate(ctx context.Context, input models....
method SiteDestroy (line 17) | func (r *mutationResolver) SiteDestroy(ctx context.Context, input models...
FILE: internal/api/resolver_mutation_studio.go
method StudioCreate (line 11) | func (r *mutationResolver) StudioCreate(ctx context.Context, input model...
method StudioUpdate (line 15) | func (r *mutationResolver) StudioUpdate(ctx context.Context, input model...
method StudioDestroy (line 19) | func (r *mutationResolver) StudioDestroy(ctx context.Context, input mode...
method FavoriteStudio (line 24) | func (r *mutationResolver) FavoriteStudio(ctx context.Context, id uuid.U...
FILE: internal/api/resolver_mutation_tag.go
method TagCreate (line 9) | func (r *mutationResolver) TagCreate(ctx context.Context, input models.T...
method TagUpdate (line 13) | func (r *mutationResolver) TagUpdate(ctx context.Context, input models.T...
method TagDestroy (line 17) | func (r *mutationResolver) TagDestroy(ctx context.Context, input models....
FILE: internal/api/resolver_mutation_tag_category.go
method TagCategoryCreate (line 9) | func (r *mutationResolver) TagCategoryCreate(ctx context.Context, input ...
method TagCategoryUpdate (line 13) | func (r *mutationResolver) TagCategoryUpdate(ctx context.Context, input ...
method TagCategoryDestroy (line 17) | func (r *mutationResolver) TagCategoryDestroy(ctx context.Context, input...
FILE: internal/api/resolver_mutation_user.go
method UserCreate (line 11) | func (r *mutationResolver) UserCreate(ctx context.Context, input models....
method UserUpdate (line 15) | func (r *mutationResolver) UserUpdate(ctx context.Context, input models....
method UserDestroy (line 19) | func (r *mutationResolver) UserDestroy(ctx context.Context, input models...
method RegenerateAPIKey (line 24) | func (r *mutationResolver) RegenerateAPIKey(ctx context.Context, userID ...
method ResetPassword (line 28) | func (r *mutationResolver) ResetPassword(ctx context.Context, input mode...
method ChangePassword (line 33) | func (r *mutationResolver) ChangePassword(ctx context.Context, input mod...
method NewUser (line 38) | func (r *mutationResolver) NewUser(ctx context.Context, input models.New...
method ActivateNewUser (line 42) | func (r *mutationResolver) ActivateNewUser(ctx context.Context, input mo...
method GenerateInviteCodes (line 46) | func (r *mutationResolver) GenerateInviteCodes(ctx context.Context, inpu...
method GenerateInviteCode (line 50) | func (r *mutationResolver) GenerateInviteCode(ctx context.Context) (*uui...
method RescindInviteCode (line 54) | func (r *mutationResolver) RescindInviteCode(ctx context.Context, invite...
method GrantInvite (line 59) | func (r *mutationResolver) GrantInvite(ctx context.Context, input models...
method RevokeInvite (line 63) | func (r *mutationResolver) RevokeInvite(ctx context.Context, input model...
method RequestChangeEmail (line 67) | func (r *mutationResolver) RequestChangeEmail(ctx context.Context) (mode...
method ValidateChangeEmail (line 71) | func (r *mutationResolver) ValidateChangeEmail(ctx context.Context, toke...
method ConfirmChangeEmail (line 75) | func (r *mutationResolver) ConfirmChangeEmail(ctx context.Context, token...
FILE: internal/api/resolver_query_draft.go
method FindDrafts (line 12) | func (r *queryResolver) FindDrafts(ctx context.Context) ([]models.Draft,...
method FindDraft (line 17) | func (r *queryResolver) FindDraft(ctx context.Context, id uuid.UUID) (*m...
FILE: internal/api/resolver_query_edit.go
method FindEdit (line 10) | func (r *queryResolver) FindEdit(ctx context.Context, id uuid.UUID) (*mo...
method QueryEdits (line 14) | func (r *queryResolver) QueryEdits(ctx context.Context, input models.Edi...
type queryEditResolver (line 20) | type queryEditResolver struct
method Count (line 22) | func (r *queryEditResolver) Count(ctx context.Context, obj *models.Edi...
method Edits (line 26) | func (r *queryEditResolver) Edits(ctx context.Context, obj *models.Edi...
FILE: internal/api/resolver_query_mod_audit.go
method QueryModAudits (line 9) | func (r *queryResolver) QueryModAudits(ctx context.Context, input models...
type queryModAuditResolver (line 15) | type queryModAuditResolver struct
method Count (line 17) | func (r *queryModAuditResolver) Count(ctx context.Context, obj *models...
method Audits (line 21) | func (r *queryModAuditResolver) Audits(ctx context.Context, obj *model...
type modAuditResolver (line 25) | type modAuditResolver struct
method Action (line 27) | func (r *modAuditResolver) Action(ctx context.Context, obj *models.Mod...
method User (line 31) | func (r *modAuditResolver) User(ctx context.Context, obj *models.ModAu...
FILE: internal/api/resolver_query_notifications.go
method QueryNotifications (line 10) | func (r *queryResolver) QueryNotifications(ctx context.Context, input mo...
type queryNotificationsResolver (line 16) | type queryNotificationsResolver struct
method Count (line 18) | func (r *queryNotificationsResolver) Count(ctx context.Context, query ...
method Notifications (line 24) | func (r *queryNotificationsResolver) Notifications(ctx context.Context...
method GetUnreadNotificationCount (line 32) | func (r *queryResolver) GetUnreadNotificationCount(ctx context.Context) ...
FILE: internal/api/resolver_query_performer.go
method FindPerformer (line 11) | func (r *queryResolver) FindPerformer(ctx context.Context, id uuid.UUID)...
method QueryPerformers (line 15) | func (r *queryResolver) QueryPerformers(ctx context.Context, input model...
type queryPerformerResolver (line 21) | type queryPerformerResolver struct
method Count (line 23) | func (r *queryPerformerResolver) Count(ctx context.Context, obj *model...
method Performers (line 30) | func (r *queryPerformerResolver) Performers(ctx context.Context, obj *...
method Facets (line 37) | func (r *queryPerformerResolver) Facets(ctx context.Context, obj *mode...
method QueryExistingPerformer (line 44) | func (r *queryResolver) QueryExistingPerformer(ctx context.Context, inpu...
type queryExistingPerformerResolver (line 50) | type queryExistingPerformerResolver struct
method Edits (line 52) | func (r *queryExistingPerformerResolver) Edits(ctx context.Context, ob...
method Performers (line 56) | func (r *queryExistingPerformerResolver) Performers(ctx context.Contex...
method SearchPerformer (line 61) | func (r *queryResolver) SearchPerformer(ctx context.Context, term string...
method SearchPerformers (line 72) | func (r *queryResolver) SearchPerformers(ctx context.Context, term strin...
FILE: internal/api/resolver_query_scene.go
method FindScene (line 13) | func (r *queryResolver) FindScene(ctx context.Context, id uuid.UUID) (*m...
method QueryScenes (line 17) | func (r *queryResolver) QueryScenes(ctx context.Context, input models.Sc...
method FindScenesBySceneFingerprints (line 23) | func (r *queryResolver) FindScenesBySceneFingerprints(ctx context.Contex...
type querySceneResolver (line 32) | type querySceneResolver struct
method Count (line 34) | func (r *querySceneResolver) Count(ctx context.Context, obj *models.Sc...
method Scenes (line 41) | func (r *querySceneResolver) Scenes(ctx context.Context, obj *models.S...
method QueryExistingScene (line 48) | func (r *queryResolver) QueryExistingScene(ctx context.Context, input mo...
type queryExistingSceneResolver (line 55) | type queryExistingSceneResolver struct
method Edits (line 57) | func (r *queryExistingSceneResolver) Edits(ctx context.Context, obj *m...
method Scenes (line 61) | func (r *queryExistingSceneResolver) Scenes(ctx context.Context, obj *...
method SearchScene (line 66) | func (r *queryResolver) SearchScene(ctx context.Context, term string, li...
method SearchScenes (line 77) | func (r *queryResolver) SearchScenes(ctx context.Context, term string, l...
method searchScenes (line 81) | func (r *queryResolver) searchScenes(ctx context.Context, term string, l...
FILE: internal/api/resolver_query_site.go
method FindSite (line 11) | func (r *queryResolver) FindSite(ctx context.Context, id uuid.UUID) (*mo...
method QuerySites (line 15) | func (r *queryResolver) QuerySites(ctx context.Context) (*models.QuerySi...
FILE: internal/api/resolver_query_studio.go
method FindStudio (line 11) | func (r *queryResolver) FindStudio(ctx context.Context, id *uuid.UUID, n...
method QueryStudios (line 21) | func (r *queryResolver) QueryStudios(ctx context.Context, input models.S...
method SearchStudio (line 25) | func (r *queryResolver) SearchStudio(ctx context.Context, term string, l...
FILE: internal/api/resolver_query_tag.go
method FindTag (line 12) | func (r *queryResolver) FindTag(ctx context.Context, id *uuid.UUID, name...
method FindTagOrAlias (line 23) | func (r *queryResolver) FindTagOrAlias(ctx context.Context, name string)...
method QueryTags (line 27) | func (r *queryResolver) QueryTags(ctx context.Context, input models.TagQ...
method SearchTag (line 46) | func (r *queryResolver) SearchTag(ctx context.Context, term string, limi...
FILE: internal/api/resolver_query_tag_category.go
method FindTagCategory (line 11) | func (r *queryResolver) FindTagCategory(ctx context.Context, id uuid.UUI...
method QueryTagCategories (line 15) | func (r *queryResolver) QueryTagCategories(ctx context.Context) (*models...
FILE: internal/api/resolver_query_user.go
method FindUser (line 12) | func (r *queryResolver) FindUser(ctx context.Context, id *uuid.UUID, use...
method QueryUsers (line 24) | func (r *queryResolver) QueryUsers(ctx context.Context, input models.Use...
method Me (line 28) | func (r *queryResolver) Me(ctx context.Context) (*models.User, error) {
FILE: internal/api/routes_image.go
type imageRoutes (line 25) | type imageRoutes struct
method Routes (line 29) | func (rs imageRoutes) Routes() chi.Router {
method image (line 38) | func (rs imageRoutes) image(w http.ResponseWriter, r *http.Request) {
method siteImage (line 131) | func (rs imageRoutes) siteImage(w http.ResponseWriter, r *http.Request) {
function getImageSize (line 164) | func getImageSize(r *http.Request) (int, error) {
function shouldResize (line 186) | func shouldResize(image *models.Image, requestedSize int) bool {
FILE: internal/api/routes_root.go
type rootRoutes (line 15) | type rootRoutes struct
method Routes (line 20) | func (rr rootRoutes) Routes(fac service.Factory) chi.Router {
method assets (line 44) | func (rr rootRoutes) assets(w http.ResponseWriter, r *http.Request) {
method app (line 53) | func (rr rootRoutes) app(w http.ResponseWriter, r *http.Request) {
function getIndex (line 65) | func getIndex(ui embed.FS) []byte {
FILE: internal/api/scene_edit_integration_test.go
type sceneEditTestRunner (line 14) | type sceneEditTestRunner struct
method testCreateSceneEdit (line 24) | func (s *sceneEditTestRunner) testCreateSceneEdit() {
method verifyCreatedSceneEdit (line 32) | func (s *sceneEditTestRunner) verifyCreatedSceneEdit(input models.Scen...
method testFindEditById (line 43) | func (s *sceneEditTestRunner) testFindEditById() {
method testModifySceneEdit (line 52) | func (s *sceneEditTestRunner) testModifySceneEdit() {
method verifyUpdatedSceneEdit (line 79) | func (s *sceneEditTestRunner) verifyUpdatedSceneEdit(originalScene *sc...
method verifySceneEditDetails (line 88) | func (s *sceneEditTestRunner) verifySceneEditDetails(input models.Scen...
method verifySceneEdit (line 110) | func (s *sceneEditTestRunner) verifySceneEdit(input models.SceneEditDe...
method testDestroySceneEdit (line 160) | func (s *sceneEditTestRunner) testDestroySceneEdit() {
method verifyDestroySceneEdit (line 177) | func (s *sceneEditTestRunner) verifyDestroySceneEdit(sceneID uuid.UUID...
method testMergeSceneEdit (line 187) | func (s *sceneEditTestRunner) testMergeSceneEdit() {
method verifyMergeSceneEdit (line 215) | func (s *sceneEditTestRunner) verifyMergeSceneEdit(originalScene *scen...
method testApplyCreateSceneEdit (line 232) | func (s *sceneEditTestRunner) testApplyCreateSceneEdit() {
method verifyAppliedSceneCreateEdit (line 240) | func (s *sceneEditTestRunner) verifyAppliedSceneCreateEdit(input model...
method testApplyModifySceneEdit (line 252) | func (s *sceneEditTestRunner) testApplyModifySceneEdit() {
method verifyApplyModifySceneEdit (line 290) | func (s *sceneEditTestRunner) verifyApplyModifySceneEdit(input models....
method testApplyModifyUnsetSceneEdit (line 299) | func (s *sceneEditTestRunner) testApplyModifyUnsetSceneEdit() {
method testApplyDestroySceneEdit (line 353) | func (s *sceneEditTestRunner) testApplyDestroySceneEdit() {
method verifyApplyDestroySceneEdit (line 372) | func (s *sceneEditTestRunner) verifyApplyDestroySceneEdit(destroyedSce...
method testApplyMergeSceneEdit (line 381) | func (s *sceneEditTestRunner) testApplyMergeSceneEdit() {
method verifyAppliedMergeSceneEdit (line 413) | func (s *sceneEditTestRunner) verifyAppliedMergeSceneEdit(input models...
method testQueryExistingScene (line 428) | func (s *sceneEditTestRunner) testQueryExistingScene() {
method testSceneEditUpdate (line 495) | func (s *sceneEditTestRunner) testSceneEditUpdate() {
function createSceneEditTestRunner (line 18) | func createSceneEditTestRunner(t *testing.T) *sceneEditTestRunner {
function TestCreateSceneEdit (line 519) | func TestCreateSceneEdit(t *testing.T) {
function TestModifySceneEdit (line 524) | func TestModifySceneEdit(t *testing.T) {
function TestDestroySceneEdit (line 529) | func TestDestroySceneEdit(t *testing.T) {
function TestMergeSceneEdit (line 534) | func TestMergeSceneEdit(t *testing.T) {
function TestApplyCreateSceneEdit (line 539) | func TestApplyCreateSceneEdit(t *testing.T) {
function TestApplyModifySceneEdit (line 544) | func TestApplyModifySceneEdit(t *testing.T) {
function TestApplyModifyUnsetSceneEdit (line 549) | func TestApplyModifyUnsetSceneEdit(t *testing.T) {
function TestApplyDestroySceneEdit (line 554) | func TestApplyDestroySceneEdit(t *testing.T) {
function TestApplyMergeSceneEdit (line 559) | func TestApplyMergeSceneEdit(t *testing.T) {
function TestQueryExistingScene (line 564) | func TestQueryExistingScene(t *testing.T) {
function TestSceneEditUpdate (line 569) | func TestSceneEditUpdate(t *testing.T) {
FILE: internal/api/scene_integration_test.go
type sceneTestRunner (line 15) | type sceneTestRunner struct
method testCreateScene (line 25) | func (s *sceneTestRunner) testCreateScene() {
method verifyCreatedScene (line 74) | func (s *sceneTestRunner) verifyCreatedScene(input models.SceneCreateI...
method testFindSceneById (line 90) | func (s *sceneTestRunner) testFindSceneById() {
method testUpdateScene (line 104) | func (s *sceneTestRunner) testUpdateScene() {
method verifyUpdatedScene (line 222) | func (s *sceneTestRunner) verifyUpdatedScene(input models.SceneUpdateI...
method verifyUpdatedFingerprints (line 236) | func (s *sceneTestRunner) verifyUpdatedFingerprints(original, updated ...
method testDestroyScene (line 277) | func (s *sceneTestRunner) testDestroyScene() {
method testSubmitFingerprint (line 297) | func (s *sceneTestRunner) testSubmitFingerprint() {
method testSubmitFingerprintUnmatch (line 344) | func (s *sceneTestRunner) testSubmitFingerprintUnmatch() {
method testSubmitFingerprintModify (line 366) | func (s *sceneTestRunner) testSubmitFingerprintModify() {
method testSubmitFingerprintUnmatchModify (line 426) | func (s *sceneTestRunner) testSubmitFingerprintUnmatchModify() {
method verifyQueryScenesResult (line 481) | func (s *sceneTestRunner) verifyQueryScenesResult(filter models.SceneQ...
method verifyInvalidModifier (line 507) | func (s *sceneTestRunner) verifyInvalidModifier(filter models.SceneQue...
method testQueryScenesByStudio (line 518) | func (s *sceneTestRunner) testQueryScenesByStudio() {
method testQueryScenesByPerformer (line 596) | func (s *sceneTestRunner) testQueryScenesByPerformer() {
method testQueryScenesByTag (line 680) | func (s *sceneTestRunner) testQueryScenesByTag() {
method testSubmitFingerprintsBatch (line 833) | func (s *sceneTestRunner) testSubmitFingerprintsBatch() {
method testSubmitFingerprintsBatchMixedResults (line 878) | func (s *sceneTestRunner) testSubmitFingerprintsBatchMixedResults() {
method testSubmitFingerprintsBatchMaxLimit (line 921) | func (s *sceneTestRunner) testSubmitFingerprintsBatchMaxLimit() {
method testFindScenesBySceneFingerprints (line 943) | func (s *sceneTestRunner) testFindScenesBySceneFingerprints() {
method testMoveFingerprintSubmissions (line 1010) | func (s *sceneTestRunner) testMoveFingerprintSubmissions() {
method testDeleteFingerprintSubmissions (line 1088) | func (s *sceneTestRunner) testDeleteFingerprintSubmissions() {
method testFindScenesBySceneFingerprintsMultipleMatches (line 1158) | func (s *sceneTestRunner) testFindScenesBySceneFingerprintsMultipleMat...
function createSceneTestRunner (line 19) | func createSceneTestRunner(t *testing.T) *sceneTestRunner {
function TestCreateScene (line 760) | func TestCreateScene(t *testing.T) {
function TestFindSceneById (line 765) | func TestFindSceneById(t *testing.T) {
function TestUpdateScene (line 770) | func TestUpdateScene(t *testing.T) {
function TestDestroyScene (line 778) | func TestDestroyScene(t *testing.T) {
function TestQueryScenesByStudio (line 783) | func TestQueryScenesByStudio(t *testing.T) {
function TestQueryScenesByPerformer (line 788) | func TestQueryScenesByPerformer(t *testing.T) {
function TestQueryScenesByTag (line 793) | func TestQueryScenesByTag(t *testing.T) {
function TestSubmitFingerprint (line 798) | func TestSubmitFingerprint(t *testing.T) {
function TestSubmitFingerprintUnmatch (line 803) | func TestSubmitFingerprintUnmatch(t *testing.T) {
function TestSubmitFingerprintModify (line 808) | func TestSubmitFingerprintModify(t *testing.T) {
function TestSubmitFingerprintUnmatchModify (line 813) | func TestSubmitFingerprintUnmatchModify(t *testing.T) {
function TestSubmitFingerprintsBatch (line 818) | func TestSubmitFingerprintsBatch(t *testing.T) {
function TestSubmitFingerprintsBatchMixedResults (line 823) | func TestSubmitFingerprintsBatchMixedResults(t *testing.T) {
function TestSubmitFingerprintsBatchMaxLimit (line 828) | func TestSubmitFingerprintsBatchMaxLimit(t *testing.T) {
function TestFindScenesBySceneFingerprints (line 1005) | func TestFindScenesBySceneFingerprints(t *testing.T) {
function TestMoveFingerprintSubmissions (line 1148) | func TestMoveFingerprintSubmissions(t *testing.T) {
function TestDeleteFingerprintSubmissions (line 1153) | func TestDeleteFingerprintSubmissions(t *testing.T) {
function TestFindScenesBySceneFingerprintsMultipleMatches (line 1244) | func TestFindScenesBySceneFingerprintsMultipleMatches(t *testing.T) {
FILE: internal/api/search_integration_test.go
type searchTestRunner (line 12) | type searchTestRunner struct
method testSearchPerformerByTerm (line 22) | func (s *searchTestRunner) testSearchPerformerByTerm() {
method testSearchPerformerByID (line 38) | func (s *searchTestRunner) testSearchPerformerByID() {
method testSearchPerformerByNonExistentID (line 54) | func (s *searchTestRunner) testSearchPerformerByNonExistentID() {
method testSearchSceneByTerm (line 62) | func (s *searchTestRunner) testSearchSceneByTerm() {
method testSearchSceneByID (line 88) | func (s *searchTestRunner) testSearchSceneByID() {
method testSearchTagByTerm (line 104) | func (s *searchTestRunner) testSearchTagByTerm() {
method testSearchTagByID (line 118) | func (s *searchTestRunner) testSearchTagByID() {
method testSearchPerformerFacets (line 167) | func (s *searchTestRunner) testSearchPerformerFacets() {
method testQueryPerformerNoFacets (line 196) | func (s *searchTestRunner) testQueryPerformerNoFacets() {
function createSearchTestRunner (line 16) | func createSearchTestRunner(t *testing.T) *searchTestRunner {
function TestSearchPerformerByTerm (line 132) | func TestSearchPerformerByTerm(t *testing.T) {
function TestSearchPerformerByID (line 137) | func TestSearchPerformerByID(t *testing.T) {
function TestSearchPerformerByNonExistentID (line 142) | func TestSearchPerformerByNonExistentID(t *testing.T) {
function TestSearchSceneByTerm (line 147) | func TestSearchSceneByTerm(t *testing.T) {
function TestSearchSceneByID (line 152) | func TestSearchSceneByID(t *testing.T) {
function TestSearchTagByTerm (line 157) | func TestSearchTagByTerm(t *testing.T) {
function TestSearchTagByID (line 162) | func TestSearchTagByID(t *testing.T) {
function TestSearchPerformerFacets (line 211) | func TestSearchPerformerFacets(t *testing.T) {
function TestQueryPerformerNoFacets (line 216) | func TestQueryPerformerNoFacets(t *testing.T) {
FILE: internal/api/server.go
constant APIKeyHeader (line 48) | APIKeyHeader = "ApiKey"
function getUserAndRoles (line 50) | func getUserAndRoles(ctx context.Context, fac service.Factory, userID st...
function authenticateHandler (line 71) | func authenticateHandler(fac service.Factory) func(http.Handler) http.Ha...
function redirect (line 128) | func redirect(w http.ResponseWriter, req *http.Request) {
function Start (line 136) | func Start(fac service.Factory, ui embed.FS) {
function printVersion (line 287) | func printVersion() {
function GetVersion (line 295) | func GetVersion() (string, string, string) {
function makeTLSConfig (line 299) | func makeTLSConfig() *tls.Config {
type contextKey (line 323) | type contextKey struct
function BaseURLMiddleware (line 331) | func BaseURLMiddleware(next http.Handler) http.Handler {
FILE: internal/api/session.go
constant cookieName (line 14) | cookieName = "stashbox"
constant usernameFormKey (line 15) | usernameFormKey = "username"
constant passwordFormKey (line 16) | passwordFormKey = "password"
constant userIDKey (line 17) | userIDKey = "userID"
constant maxCookieAge (line 18) | maxCookieAge = 60 * 60 * 24 * 30
function InitializeSession (line 22) | func InitializeSession() {
function handleLogin (line 26) | func handleLogin(fac service.Factory) func(http.ResponseWriter, *http.Re...
function handleLogout (line 67) | func handleLogout(w http.ResponseWriter, r *http.Request) {
function getSessionUserID (line 94) | func getSessionUserID(w http.ResponseWriter, r *http.Request) (string, e...
FILE: internal/api/site_integration_test.go
type siteTestRunner (line 13) | type siteTestRunner struct
method testCreateSite (line 23) | func (s *siteTestRunner) testCreateSite() {
method verifyCreatedSite (line 42) | func (s *siteTestRunner) verifyCreatedSite(input models.SiteCreateInpu...
method testFindSiteById (line 67) | func (s *siteTestRunner) testFindSiteById() {
method testQuerySites (line 83) | func (s *siteTestRunner) testQuerySites() {
method testUpdateSite (line 113) | func (s *siteTestRunner) testUpdateSite() {
method verifyUpdatedSite (line 137) | func (s *siteTestRunner) verifyUpdatedSite(input models.SiteUpdateInpu...
method testDestroySite (line 155) | func (s *siteTestRunner) testDestroySite() {
function createSiteTestRunner (line 17) | func createSiteTestRunner(t *testing.T) *siteTestRunner {
function TestCreateSite (line 175) | func TestCreateSite(t *testing.T) {
function TestFindSiteById (line 180) | func TestFindSiteById(t *testing.T) {
function TestQuerySites (line 185) | func TestQuerySites(t *testing.T) {
function TestUpdateSite (line 190) | func TestUpdateSite(t *testing.T) {
function TestDestroySite (line 195) | func TestDestroySite(t *testing.T) {
FILE: internal/api/studio_edit_integration_test.go
type studioEditTestRunner (line 14) | type studioEditTestRunner struct
method testCreateStudioEdit (line 24) | func (s *studioEditTestRunner) testCreateStudioEdit() {
method verifyCreatedStudioEdit (line 40) | func (s *studioEditTestRunner) verifyCreatedStudioEdit(input models.St...
method testFindEditById (line 58) | func (s *studioEditTestRunner) testFindEditById() {
method testModifyStudioEdit (line 69) | func (s *studioEditTestRunner) testModifyStudioEdit() {
method verifyUpdatedStudioEdit (line 112) | func (s *studioEditTestRunner) verifyUpdatedStudioEdit(originalStudio ...
method testDestroyStudioEdit (line 127) | func (s *studioEditTestRunner) testDestroyStudioEdit() {
method verifyDestroyStudioEdit (line 144) | func (s *studioEditTestRunner) verifyDestroyStudioEdit(studioID uuid.U...
method testMergeStudioEdit (line 155) | func (s *studioEditTestRunner) testMergeStudioEdit() {
method verifyMergeStudioEdit (line 183) | func (s *studioEditTestRunner) verifyMergeStudioEdit(originalStudio *s...
method testApplyCreateStudioEdit (line 203) | func (s *studioEditTestRunner) testApplyCreateStudioEdit() {
method verifyAppliedStudioCreateEdit (line 221) | func (s *studioEditTestRunner) verifyAppliedStudioCreateEdit(input mod...
method testApplyModifyStudioEdit (line 236) | func (s *studioEditTestRunner) testApplyModifyStudioEdit() {
method verifyApplyModifyStudioEdit (line 283) | func (s *studioEditTestRunner) verifyApplyModifyStudioEdit(input model...
method testApplyModifyUnsetStudioEdit (line 297) | func (s *studioEditTestRunner) testApplyModifyUnsetStudioEdit() {
method testApplyDestroyStudioEdit (line 368) | func (s *studioEditTestRunner) testApplyDestroyStudioEdit() {
method verifyApplyDestroyStudioEdit (line 400) | func (s *studioEditTestRunner) verifyApplyDestroyStudioEdit(destroyedS...
method testApplyMergeStudioEdit (line 410) | func (s *studioEditTestRunner) testApplyMergeStudioEdit() {
method verifyAppliedMergeStudioEdit (line 464) | func (s *studioEditTestRunner) verifyAppliedMergeStudioEdit(input mode...
method testStudioEditUpdate (line 484) | func (s *studioEditTestRunner) testStudioEditUpdate() {
function createStudioEditTestRunner (line 18) | func createStudioEditTestRunner(t *testing.T) *studioEditTestRunner {
function TestCreateStudioEdit (line 511) | func TestCreateStudioEdit(t *testing.T) {
function TestModifyStudioEdit (line 516) | func TestModifyStudioEdit(t *testing.T) {
function TestDestroyStudioEdit (line 521) | func TestDestroyStudioEdit(t *testing.T) {
function TestMergeStudioEdit (line 526) | func TestMergeStudioEdit(t *testing.T) {
function TestApplyCreateStudioEdit (line 531) | func TestApplyCreateStudioEdit(t *testing.T) {
function TestApplyModifyStudioEdit (line 536) | func TestApplyModifyStudioEdit(t *testing.T) {
function TestApplyModifyUnsetStudioEdit (line 541) | func TestApplyModifyUnsetStudioEdit(t *testing.T) {
function TestApplyDestroyStudioEdit (line 546) | func TestApplyDestroyStudioEdit(t *testing.T) {
function TestApplyMergeStudioEdit (line 551) | func TestApplyMergeStudioEdit(t *testing.T) {
function TestStudioEditUpdate (line 556) | func TestStudioEditUpdate(t *testing.T) {
FILE: internal/api/studio_integration_test.go
type studioTestRunner (line 14) | type studioTestRunner struct
method generateStudioName (line 25) | func (s *studioTestRunner) generateStudioName() string {
method testCreateStudio (line 30) | func (s *studioTestRunner) testCreateStudio() {
method verifyCreatedStudio (line 41) | func (s *studioTestRunner) verifyCreatedStudio(input models.StudioCrea...
method testFindStudioById (line 48) | func (s *studioTestRunner) testFindStudioById() {
method testFindStudioByName (line 63) | func (s *studioTestRunner) testFindStudioByName() {
method testUpdateStudioName (line 78) | func (s *studioTestRunner) testUpdateStudioName() {
method verifyUpdatedStudio (line 105) | func (s *studioTestRunner) verifyUpdatedStudio(input models.StudioUpda...
method testDestroyStudio (line 110) | func (s *studioTestRunner) testDestroyStudio() {
method testQueryStudios (line 132) | func (s *studioTestRunner) testQueryStudios() {
method testParentChildStudios (line 211) | func (s *studioTestRunner) testParentChildStudios() {
function createStudioTestRunner (line 19) | func createStudioTestRunner(t *testing.T) *studioTestRunner {
function TestCreateStudio (line 181) | func TestCreateStudio(t *testing.T) {
function TestFindStudioById (line 186) | func TestFindStudioById(t *testing.T) {
function TestFindStudioByName (line 191) | func TestFindStudioByName(t *testing.T) {
function TestUpdateStudioName (line 196) | func TestUpdateStudioName(t *testing.T) {
function TestDestroyStudio (line 201) | func TestDestroyStudio(t *testing.T) {
function TestQueryStudios (line 206) | func TestQueryStudios(t *testing.T) {
function TestParentChildStudios (line 274) | func TestParentChildStudios(t *testing.T) {
FILE: internal/api/tag_category_integration_test.go
type tagCategoryTestRunner (line 13) | type tagCategoryTestRunner struct
method testCreateTagCategory (line 23) | func (s *tagCategoryTestRunner) testCreateTagCategory() {
method verifyCreatedTagCategory (line 38) | func (s *tagCategoryTestRunner) verifyCreatedTagCategory(input models....
method testFindTagCategoryById (line 52) | func (s *tagCategoryTestRunner) testFindTagCategoryById() {
method testUpdateTagCategory (line 66) | func (s *tagCategoryTestRunner) testUpdateTagCategory() {
method verifyUpdatedTagCategory (line 85) | func (s *tagCategoryTestRunner) verifyUpdatedTagCategory(input models....
method testDestroyTagCategory (line 92) | func (s *tagCategoryTestRunner) testDestroyTagCategory() {
method testQueryTagCategories (line 112) | func (s *tagCategoryTestRunner) testQueryTagCategories() {
function createTagCategoryTestRunner (line 17) | func createTagCategoryTestRunner(t *testing.T) *tagCategoryTestRunner {
function TestCreateTagCategory (line 146) | func TestCreateTagCategory(t *testing.T) {
function TestUpdateTagCategory (line 151) | func TestUpdateTagCategory(t *testing.T) {
function TestDestroyTagCategory (line 156) | func TestDestroyTagCategory(t *testing.T) {
function TestQueryTagCategories (line 161) | func TestQueryTagCategories(t *testing.T) {
FILE: internal/api/tag_edit_integration_test.go
type tagEditTestRunner (line 13) | type tagEditTestRunner struct
method testCreateTagEdit (line 23) | func (s *tagEditTestRunner) testCreateTagEdit() {
method verifyCreatedTagEdit (line 42) | func (s *tagEditTestRunner) verifyCreatedTagEdit(input models.TagEditD...
method testFindEditById (line 62) | func (s *tagEditTestRunner) testFindEditById() {
method testModifyTagEdit (line 73) | func (s *tagEditTestRunner) testModifyTagEdit() {
method verifyUpdatedTagEdit (line 113) | func (s *tagEditTestRunner) verifyUpdatedTagEdit(originalTag *tagOutpu...
method testDestroyTagEdit (line 132) | func (s *tagEditTestRunner) testDestroyTagEdit() {
method verifyDestroyTagEdit (line 149) | func (s *tagEditTestRunner) verifyDestroyTagEdit(tagID uuid.UUID, edit...
method testMergeTagEdit (line 160) | func (s *tagEditTestRunner) testMergeTagEdit() {
method verifyMergeTagEdit (line 195) | func (s *tagEditTestRunner) verifyMergeTagEdit(originalTag *tagOutput,...
method testApplyCreateTagEdit (line 220) | func (s *tagEditTestRunner) testApplyCreateTagEdit() {
method verifyAppliedTagCreateEdit (line 242) | func (s *tagEditTestRunner) verifyAppliedTagCreateEdit(input models.Ta...
method testApplyModifyTagEdit (line 263) | func (s *tagEditTestRunner) testApplyModifyTagEdit() {
method verifyApplyModifyTagEdit (line 303) | func (s *tagEditTestRunner) verifyApplyModifyTagEdit(input models.TagE...
method testApplyDestroyTagEdit (line 319) | func (s *tagEditTestRunner) testApplyDestroyTagEdit() {
method verifyApplyDestroyTagEdit (line 350) | func (s *tagEditTestRunner) verifyApplyDestroyTagEdit(destroyedTag *mo...
method testApplyMergeTagEdit (line 362) | func (s *tagEditTestRunner) testApplyMergeTagEdit() {
method verifyAppliedMergeTagEdit (line 418) | func (s *tagEditTestRunner) verifyAppliedMergeTagEdit(input models.Tag...
method testTagEditUpdate (line 446) | func (s *tagEditTestRunner) testTagEditUpdate() {
function createTagEditTestRunner (line 17) | func createTagEditTestRunner(t *testing.T) *tagEditTestRunner {
function TestCreateTagEdit (line 473) | func TestCreateTagEdit(t *testing.T) {
function TestModifyTagEdit (line 478) | func TestModifyTagEdit(t *testing.T) {
function TestDestroyTagEdit (line 483) | func TestDestroyTagEdit(t *testing.T) {
function TestMergeTagEdit (line 488) | func TestMergeTagEdit(t *testing.T) {
function TestApplyCreateTagEdit (line 493) | func TestApplyCreateTagEdit(t *testing.T) {
function TestApplyModifyTagEdit (line 498) | func TestApplyModifyTagEdit(t *testing.T) {
function TestApplyDestroyTagEdit (line 503) | func TestApplyDestroyTagEdit(t *testing.T) {
function TestApplyMergeTagEdit (line 508) | func TestApplyMergeTagEdit(t *testing.T) {
function TestTagEditUpdate (line 513) | func TestTagEditUpdate(t *testing.T) {
FILE: internal/api/tag_integration_test.go
type tagTestRunner (line 13) | type tagTestRunner struct
method testCreateTag (line 23) | func (s *tagTestRunner) testCreateTag() {
method verifyCreatedTag (line 37) | func (s *tagTestRunner) verifyCreatedTag(input models.TagCreateInput, ...
method testFindTagById (line 45) | func (s *tagTestRunner) testFindTagById() {
method testFindTagByName (line 60) | func (s *tagTestRunner) testFindTagByName() {
method testUpdateTag (line 76) | func (s *tagTestRunner) testUpdateTag() {
method verifyUpdatedTag (line 96) | func (s *tagTestRunner) verifyUpdatedTag(input models.TagUpdateInput, ...
method testDestroyTag (line 102) | func (s *tagTestRunner) testDestroyTag() {
method testQueryTags (line 124) | func (s *tagTestRunner) testQueryTags() {
method testFindTagOrAlias (line 173) | func (s *tagTestRunner) testFindTagOrAlias() {
function createTagTestRunner (line 17) | func createTagTestRunner(t *testing.T) *tagTestRunner {
function TestCreateTag (line 203) | func TestCreateTag(t *testing.T) {
function TestFindTagById (line 208) | func TestFindTagById(t *testing.T) {
function TestFindTagByName (line 213) | func TestFindTagByName(t *testing.T) {
function TestUpdateTag (line 218) | func TestUpdateTag(t *testing.T) {
function TestDestroyTag (line 223) | func TestDestroyTag(t *testing.T) {
function TestQueryTags (line 228) | func TestQueryTags(t *testing.T) {
function TestFindTagOrAlias (line 233) | func TestFindTagOrAlias(t *testing.T) {
FILE: internal/api/user_integration_test.go
type userTestRunner (line 16) | type userTestRunner struct
method testCreateUser (line 26) | func (s *userTestRunner) testCreateUser() {
method verifyCreatedUser (line 43) | func (s *userTestRunner) verifyCreatedUser(input models.UserCreateInpu...
method testFindUserById (line 57) | func (s *userTestRunner) testFindUserById() {
method testFindUserByName (line 71) | func (s *userTestRunner) testFindUserByName() {
method testQueryUserByName (line 86) | func (s *userTestRunner) testQueryUserByName() {
method testUpdateUserName (line 108) | func (s *userTestRunner) testUpdateUserName() {
method testUpdatePassword (line 138) | func (s *userTestRunner) testUpdatePassword() {
method verifyUpdatedUser (line 170) | func (s *userTestRunner) verifyUpdatedUser(input models.UserUpdateInpu...
method testDestroyUser (line 175) | func (s *userTestRunner) testDestroyUser() {
method testUserQuery (line 195) | func (s *userTestRunner) testUserQuery() {
method testChangePassword (line 210) | func (s *userTestRunner) testChangePassword() {
method testRegenerateAPIKey (line 247) | func (s *userTestRunner) testRegenerateAPIKey() {
method testUserEditQuery (line 284) | func (s *userTestRunner) testUserEditQuery() {
method testMeQuery (line 353) | func (s *userTestRunner) testMeQuery() {
method testFavoritePerformer (line 363) | func (s *userTestRunner) testFavoritePerformer() {
method testFavoriteStudio (line 381) | func (s *userTestRunner) testFavoriteStudio() {
method testQueryNotifications (line 414) | func (s *userTestRunner) testQueryNotifications() {
method testGetUnreadNotificationCount (line 426) | func (s *userTestRunner) testGetUnreadNotificationCount() {
method testUpdateNotificationSubscriptions (line 432) | func (s *userTestRunner) testUpdateNotificationSubscriptions() {
method testNewUser (line 458) | func (s *userTestRunner) testNewUser() {
function createUserTestRunner (line 20) | func createUserTestRunner(t *testing.T) *userTestRunner {
function TestCreateUser (line 298) | func TestCreateUser(t *testing.T) {
function TestFindUserById (line 303) | func TestFindUserById(t *testing.T) {
function TestFindUserByName (line 308) | func TestFindUserByName(t *testing.T) {
function TestQueryUserByName (line 313) | func TestQueryUserByName(t *testing.T) {
function TestUpdateUserName (line 318) | func TestUpdateUserName(t *testing.T) {
function TestUpdateUserPassword (line 323) | func TestUpdateUserPassword(t *testing.T) {
function TestDestroyUser (line 328) | func TestDestroyUser(t *testing.T) {
function TestUserQuery (line 333) | func TestUserQuery(t *testing.T) {
function TestChangePassword (line 338) | func TestChangePassword(t *testing.T) {
function TestRegenerateAPIKey (line 343) | func TestRegenerateAPIKey(t *testing.T) {
function TestUserEditQuery (line 348) | func TestUserEditQuery(t *testing.T) {
function TestMeQuery (line 399) | func TestMeQuery(t *testing.T) {
function TestFavoritePerformer (line 404) | func TestFavoritePerformer(t *testing.T) {
function TestFavoriteStudio (line 409) | func TestFavoriteStudio(t *testing.T) {
function TestQueryNotifications (line 443) | func TestQueryNotifications(t *testing.T) {
function TestGetUnreadNotificationCount (line 448) | func TestGetUnreadNotificationCount(t *testing.T) {
function TestUpdateNotificationSubscriptions (line 453) | func TestUpdateNotificationSubscriptions(t *testing.T) {
function TestNewUser (line 506) | func TestNewUser(t *testing.T) {
FILE: internal/api/utils.go
function parseUUID (line 11) | func parseUUID(id string) uuid.UUID {
function resolveFuzzyDate (line 16) | func resolveFuzzyDate(date *string) *models.FuzzyDate {
FILE: internal/auth/authorization.go
type key (line 11) | type key
constant ContextUser (line 14) | ContextUser key = iota
constant ContextRoles (line 15) | ContextRoles
constant APIKeyHeader (line 18) | APIKeyHeader = "ApiKey"
function GetCurrentUser (line 22) | func GetCurrentUser(ctx context.Context) *models.User {
function IsRole (line 32) | func IsRole(ctx context.Context, requiredRole models.RoleEnum) bool {
function ValidateRole (line 52) | func ValidateRole(ctx context.Context, requiredRole models.RoleEnum) err...
function ValidateInvite (line 60) | func ValidateInvite(ctx context.Context) error {
function ValidateManageInvites (line 64) | func ValidateManageInvites(ctx context.Context) error {
function ValidateAdmin (line 68) | func ValidateAdmin(ctx context.Context) error {
function ValidateOwner (line 72) | func ValidateOwner(ctx context.Context, userID uuid.UUID) error {
function ValidateUserOrAdmin (line 81) | func ValidateUserOrAdmin(ctx context.Context, userID uuid.UUID) error {
function ValidateBot (line 88) | func ValidateBot(ctx context.Context) error {
FILE: internal/autocert/autocert.go
function Init (line 22) | func Init() *tls.Config {
function HTTPHandler (line 49) | func HTTPHandler(fallback http.Handler) http.Handler {
function CheckAndRenew (line 58) | func CheckAndRenew() {
function checkAndRenew (line 65) | func checkAndRenew() {
FILE: internal/config/config.go
type S3Config (line 12) | type S3Config struct
type PostgresConfig (line 21) | type PostgresConfig struct
type OTelConfig (line 27) | type OTelConfig struct
type ImageResizeConfig (line 32) | type ImageResizeConfig struct
type AutocertConfig (line 38) | type AutocertConfig struct
type config (line 45) | type config struct
type ImageBackendType (line 143) | type ImageBackendType
constant FileBackend (line 146) | FileBackend ImageBackendType = "file"
constant S3Backend (line 147) | S3Backend ImageBackendType = "s3"
function GetDatabasePath (line 171) | func GetDatabasePath() string {
function GetHost (line 175) | func GetHost() string {
function GetPort (line 179) | func GetPort() int {
function GetProfilerPort (line 183) | func GetProfilerPort() *int {
function GetJWTSignKey (line 190) | func GetJWTSignKey() []byte {
function GetSessionStoreKey (line 194) | func GetSessionStoreKey() []byte {
function GetHTTPUpgrade (line 198) | func GetHTTPUpgrade() bool {
function GetIsProduction (line 202) | func GetIsProduction() bool {
function GetRequireInvite (line 208) | func GetRequireInvite() bool {
function GetRequireActivation (line 214) | func GetRequireActivation() bool {
function GetActivationExpiry (line 219) | func GetActivationExpiry() time.Duration {
function GetEmailCooldown (line 225) | func GetEmailCooldown() time.Duration {
function GetDefaultUserRoles (line 231) | func GetDefaultUserRoles() []string {
function GetEmailHost (line 238) | func GetEmailHost() string {
function GetEmailPort (line 242) | func GetEmailPort() int {
function GetEmailUser (line 246) | func GetEmailUser() string {
function GetEmailPassword (line 250) | func GetEmailPassword() string {
function GetEmailFrom (line 254) | func GetEmailFrom() string {
function GetHostURL (line 258) | func GetHostURL() string {
function GetGuidelinesURL (line 262) | func GetGuidelinesURL() string {
function GetImageLocation (line 267) | func GetImageLocation() string {
function GetImageBackend (line 272) | func GetImageBackend() ImageBackendType {
function GetS3Config (line 276) | func GetS3Config() *S3Config {
function GetImageResizeConfig (line 280) | func GetImageResizeConfig() *ImageResizeConfig {
function GetOTelConfig (line 284) | func GetOTelConfig() *OTelConfig {
function GetAutocertConfig (line 291) | func GetAutocertConfig() *AutocertConfig {
function GetMissingAutocertSettings (line 298) | func GetMissingAutocertSettings() []string {
function ValidateImageLocation (line 318) | func ValidateImageLocation() error {
function GetImageMaxSize (line 326) | func GetImageMaxSize() *int {
function GetImageJpegQuality (line 334) | func GetImageJpegQuality() int {
function GetLogFile (line 343) | func GetLogFile() string {
function GetUserLogFile (line 350) | func GetUserLogFile() string {
function GetLogOut (line 357) | func GetLogOut() bool {
function GetLogLevel (line 363) | func GetLogLevel() string {
function GetPHashDistance (line 374) | func GetPHashDistance() int {
function InitializeDefaults (line 378) | func InitializeDefaults() error {
function Initialize (line 406) | func Initialize() error {
function GetMissingEmailSettings (line 410) | func GetMissingEmailSettings() []string {
function GetVotePromotionThreshold (line 429) | func GetVotePromotionThreshold() *int {
function GetVoteApplicationThreshold (line 436) | func GetVoteApplicationThreshold() int {
function GetVotingPeriod (line 440) | func GetVotingPeriod() int {
function GetMinDestructiveVotingPeriod (line 444) | func GetMinDestructiveVotingPeriod() int {
function GetVoteCronInterval (line 448) | func GetVoteCronInterval() string {
function GetEditUpdateLimit (line 452) | func GetEditUpdateLimit() int {
function GetRequireSceneDraft (line 456) | func GetRequireSceneDraft() bool {
function GetRequireTagRole (line 460) | func GetRequireTagRole() bool {
function GetTitle (line 464) | func GetTitle() string {
function GetFaviconPath (line 471) | func GetFaviconPath() (*string, error) {
function GetDraftTimeLimit (line 478) | func GetDraftTimeLimit() int {
function GetModAuditRetentionDays (line 482) | func GetModAuditRetentionDays() int {
function GetMaxOpenConns (line 486) | func GetMaxOpenConns() int {
function GetMaxIdleConns (line 493) | func GetMaxIdleConns() int {
function GetConnMaxLifetime (line 500) | func GetConnMaxLifetime() int {
function GetCSP (line 504) | func GetCSP() string {
FILE: internal/config/paths.go
function GetConfigDirectory (line 7) | func GetConfigDirectory() string {
function GetDefaultDatabaseFilePath (line 11) | func GetDefaultDatabaseFilePath() string {
function GetConfigName (line 15) | func GetConfigName() string {
function GetDefaultConfigFilePath (line 19) | func GetDefaultConfigFilePath() string {
function GetSSLKey (line 23) | func GetSSLKey() string {
function GetSSLCert (line 27) | func GetSSLCert() string {
FILE: internal/converter/converter.go
function ImageToModel (line 24) | func ImageToModel(i queries.Image) models.Image {
function ImageToModelPtr (line 28) | func ImageToModelPtr(i queries.Image) *models.Image {
function ImagesToModels (line 34) | func ImagesToModels(images []queries.Image) []models.Image {
function PerformerToModel (line 39) | func PerformerToModel(p queries.Performer) models.Performer {
function PerformerToModelPtr (line 43) | func PerformerToModelPtr(p queries.Performer) *models.Performer {
function SceneToModel (line 49) | func SceneToModel(s queries.Scene) models.Scene {
function SceneToModelPtr (line 53) | func SceneToModelPtr(s queries.Scene) *models.Scene {
function SiteToModel (line 59) | func SiteToModel(s queries.Site) models.Site {
function SiteToModelPtr (line 63) | func SiteToModelPtr(s queries.Site) *models.Site {
function StudioToModel (line 69) | func StudioToModel(s queries.Studio) models.Studio {
function StudioToModelPtr (line 73) | func StudioToModelPtr(s queries.Studio) *models.Studio {
function TagCategoryToModel (line 79) | func TagCategoryToModel(tc queries.TagCategory) models.TagCategory {
function TagCategoryToModelPtr (line 83) | func TagCategoryToModelPtr(tc queries.TagCategory) *models.TagCategory {
function TagToModel (line 89) | func TagToModel(t queries.Tag) models.Tag {
function TagToModelPtr (line 93) | func TagToModelPtr(t queries.Tag) *models.Tag {
function UserTokenToModel (line 99) | func UserTokenToModel(ut queries.UserToken) models.UserToken {
function UserTokenToModelPtr (line 103) | func UserTokenToModelPtr(ut queries.UserToken) *models.UserToken {
function SceneDraftInputToSceneDraft (line 109) | func SceneDraftInputToSceneDraft(input models.SceneDraftInput) models.Sc...
function EditToModel (line 114) | func EditToModel(e queries.Edit) models.Edit {
function EditToModelPtr (line 118) | func EditToModelPtr(e queries.Edit) *models.Edit {
function EditsToModels (line 124) | func EditsToModels(edits []queries.Edit) []models.Edit {
function EditVoteToModel (line 129) | func EditVoteToModel(ec queries.EditVote) models.EditVote {
function EditCommentToModel (line 134) | func EditCommentToModel(ec queries.EditComment) models.EditComment {
function EditCommentToModelPtr (line 138) | func EditCommentToModelPtr(ec queries.EditComment) *models.EditComment {
function TagToCreateParams (line 144) | func TagToCreateParams(t models.Tag) queries.CreateTagParams {
function TagToUpdateParams (line 149) | func TagToUpdateParams(t models.Tag) queries.UpdateTagParams {
function StudioToCreateParams (line 154) | func StudioToCreateParams(s models.Studio) queries.CreateStudioParams {
function StudioToUpdateParams (line 159) | func StudioToUpdateParams(s models.Studio) queries.UpdateStudioParams {
function SceneToCreateParams (line 164) | func SceneToCreateParams(s models.Scene) queries.CreateSceneParams {
function SceneToUpdateParams (line 169) | func SceneToUpdateParams(s models.Scene) queries.UpdateSceneParams {
function BodyModInputToModel (line 174) | func BodyModInputToModel(inputs []models.BodyModificationInput) []models...
function PerformerToCreateParams (line 179) | func PerformerToCreateParams(p models.Performer) queries.CreatePerformer...
function PerformerToUpdateParams (line 184) | func PerformerToUpdateParams(p models.Performer) queries.UpdatePerformer...
function EditToUpdateParams (line 189) | func EditToUpdateParams(e models.Edit) queries.UpdateEditParams {
function EditToCreateParams (line 194) | func EditToCreateParams(e models.Edit) queries.CreateEditParams {
function EditCommentToCreateParams (line 199) | func EditCommentToCreateParams(ec models.EditComment) queries.CreateEdit...
function UserToModel (line 204) | func UserToModel(u queries.User) models.User {
function UserToModelPtr (line 208) | func UserToModelPtr(u queries.User) *models.User {
function PerformerCreateInputToPerformer (line 214) | func PerformerCreateInputToPerformer(input models.PerformerCreateInput) ...
function UpdatePerformerFromUpdateInput (line 237) | func UpdatePerformerFromUpdateInput(performer *models.Performer, input m...
function SceneCreateInputToScene (line 292) | func SceneCreateInputToScene(input models.SceneCreateInput) models.Scene {
function UpdateSceneFromUpdateInput (line 311) | func UpdateSceneFromUpdateInput(scene *models.Scene, input models.SceneU...
function SiteCreateInputToSite (line 339) | func SiteCreateInputToSite(input models.SiteCreateInput) models.Site {
function SiteToCreateParams (line 355) | func SiteToCreateParams(s models.Site) queries.CreateSiteParams {
function SiteToUpdateParams (line 360) | func SiteToUpdateParams(s models.Site) queries.UpdateSiteParams {
function UpdateSiteFromUpdateInput (line 365) | func UpdateSiteFromUpdateInput(site *models.Site, input models.SiteUpdat...
function StudioCreateInputToCreateParams (line 379) | func StudioCreateInputToCreateParams(input models.StudioCreateInput) (qu...
function UpdateStudioFromUpdateInput (line 398) | func UpdateStudioFromUpdateInput(studio queries.Studio, input models.Stu...
function TagCategoryCreateInputToCreateParams (line 419) | func TagCategoryCreateInputToCreateParams(input models.TagCategoryCreate...
function UpdateTagCategoryFromUpdateInput (line 434) | func UpdateTagCategoryFromUpdateInput(tagCategory queries.TagCategory, i...
function TagCreateInputToCreateParams (line 460) | func TagCreateInputToCreateParams(input models.TagCreateInput) (queries....
function UpdateTagFromUpdateInput (line 480) | func UpdateTagFromUpdateInput(tag queries.Tag, input models.TagUpdateInp...
function UserCreateInputToCreateParams (line 502) | func UserCreateInputToCreateParams(input models.UserCreateInput, id uuid...
function UpdateUserFromUpdateInput (line 521) | func UpdateUserFromUpdateInput(user queries.User, input models.UserUpdat...
function CreateUserTokenParamsFromData (line 547) | func CreateUserTokenParamsFromData(tokenType string, data any) (queries....
function DraftToModel (line 571) | func DraftToModel(d queries.Draft) models.Draft {
function DraftToModelPtr (line 581) | func DraftToModelPtr(d queries.Draft) *models.Draft {
function CreateEditCommentParams (line 587) | func CreateEditCommentParams(editID, userID uuid.UUID, commentText strin...
function PerformersToModels (line 602) | func PerformersToModels(performers []queries.Performer) []models.Perform...
function ScenesToModels (line 606) | func ScenesToModels(scenes []queries.Scene) []models.Scene {
function StudiosToModels (line 611) | func StudiosToModels(studios []queries.Studio) []models.Studio {
function TagCategoriesToModels (line 616) | func TagCategoriesToModels(tagCategories []queries.TagCategory) []models...
function TagsToModels (line 621) | func TagsToModels(tags []queries.Tag) []models.Tag {
function EditCommentsToModels (line 626) | func EditCommentsToModels(comments []queries.EditComment) []models.EditC...
function EditVotesToModels (line 631) | func EditVotesToModels(votes []queries.EditVote) []models.EditVote {
function InviteKeysToModels (line 636) | func InviteKeysToModels(keys []queries.InviteKey) []models.InviteKey {
function NotificationsToModels (line 641) | func NotificationsToModels(notifications []queries.Notification) []model...
function InviteKeyToModel (line 646) | func InviteKeyToModel(ik queries.InviteKey) models.InviteKey {
function StringToRoleEnum (line 662) | func StringToRoleEnum(s string) *models.RoleEnum {
function StringsToRoleEnums (line 672) | func StringsToRoleEnums(strings []string) []models.RoleEnum {
function NotificationToModel (line 683) | func NotificationToModel(dbNotification queries.Notification) models.Not...
FILE: internal/converter/gen/extensions.go
function ConvertTime (line 12) | func ConvertTime(t time.Time) time.Time {
function ConvertNullIntToInt (line 16) | func ConvertNullIntToInt(i *int) int {
function ConvertNotificationType (line 23) | func ConvertNotificationType(t queries.NotificationType) models.Notifica...
FILE: internal/converter/gen/generated.go
type CreateParamsConverterImpl (line 14) | type CreateParamsConverterImpl struct
method ConvertEditCommentToCreateParams (line 16) | func (c *CreateParamsConverterImpl) ConvertEditCommentToCreateParams(s...
method ConvertEditToCreateParams (line 24) | func (c *CreateParamsConverterImpl) ConvertEditToCreateParams(source m...
method ConvertPerformerToCreateParams (line 37) | func (c *CreateParamsConverterImpl) ConvertPerformerToCreateParams(sou...
method ConvertSceneToCreateParams (line 107) | func (c *CreateParamsConverterImpl) ConvertSceneToCreateParams(source ...
method ConvertSiteToCreateParams (line 141) | func (c *CreateParamsConverterImpl) ConvertSiteToCreateParams(source m...
method ConvertStudioToCreateParams (line 165) | func (c *CreateParamsConverterImpl) ConvertStudioToCreateParams(source...
method ConvertTagToCreateParams (line 172) | func (c *CreateParamsConverterImpl) ConvertTagToCreateParams(source mo...
method jsonRawMessageToByteList (line 183) | func (c *CreateParamsConverterImpl) jsonRawMessageToByteList(source js...
method modelsBreastTypeEnumToModelsBreastTypeEnum (line 193) | func (c *CreateParamsConverterImpl) modelsBreastTypeEnumToModelsBreast...
method modelsEthnicityEnumToModelsEthnicityEnum (line 206) | func (c *CreateParamsConverterImpl) modelsEthnicityEnumToModelsEthnici...
method modelsEyeColorEnumToModelsEyeColorEnum (line 229) | func (c *CreateParamsConverterImpl) modelsEyeColorEnumToModelsEyeColor...
method modelsGenderEnumToModelsGenderEnum (line 248) | func (c *CreateParamsConverterImpl) modelsGenderEnumToModelsGenderEnum...
method modelsHairColorEnumToModelsHairColorEnum (line 267) | func (c *CreateParamsConverterImpl) modelsHairColorEnumToModelsHairCol...
method uuidNullUUIDToUuidNullUUID (line 294) | func (c *CreateParamsConverterImpl) uuidNullUUIDToUuidNullUUID(source ...
method uuidUUIDToUuidUUID (line 300) | func (c *CreateParamsConverterImpl) uuidUUIDToUuidUUID(source uuid.UUI...
type InputConverterImpl (line 308) | type InputConverterImpl struct
method ConvertBodyModInputSlice (line 310) | func (c *InputConverterImpl) ConvertBodyModInputSlice(source []models....
method ConvertSceneDraftInput (line 320) | func (c *InputConverterImpl) ConvertSceneDraftInput(source
Condensed preview — 752 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,790K chars).
[
{
"path": ".dockerignore",
"chars": 820,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
},
{
"path": ".gitattributes",
"chars": 37,
"preview": "go.mod text eol=lf\ngo.sum text eol=lf"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 972,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[Bug Report] Short Form Subject (50 Chars or less"
},
{
"path": ".github/ISSUE_TEMPLATE/discussion---request-for-commentary--rfc-.md",
"chars": 970,
"preview": "---\nname: Discussion / Request for Commentary [RFC]\nabout: This is for issues that will be discussed and won't necessari"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 650,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[Feature] Short Form Title (50 chars or less.)"
},
{
"path": ".github/workflows/build.yml",
"chars": 6451,
"preview": "name: Build\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n release:\n types: [ publi"
},
{
"path": ".github/workflows/frontend-lint.yml",
"chars": 829,
"preview": "name: Lint (frontend)\non:\n push:\n pull_request:\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n canc"
},
{
"path": ".github/workflows/golangci-lint.yml",
"chars": 638,
"preview": "name: Lint (golangci-lint)\non:\n push:\n pull_request:\n\njobs:\n golangci:\n name: lint\n runs-on: ubuntu-24.04\n\n "
},
{
"path": ".gitignore",
"chars": 812,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
},
{
"path": ".golangci.yml",
"chars": 1622,
"preview": "version: \"2\"\nrun:\n go: \"1.25\"\nlinters:\n enable:\n - copyloopvar\n - dogsled\n - errorlint\n - exhaustive\n -"
},
{
"path": "CLAUDE.md",
"chars": 7249,
"preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2019 stashapp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "Makefile",
"chars": 5297,
"preview": "LDFLAGS := $(LDFLAGS)\n\n.PHONY: \\\n\tstash-box \\\n\tgenerate \\\n\tgenerate-backend \\\n\tgenerate-ui \\\n\tgenerate-sqlc \\\n\tgenerate-"
},
{
"path": "README.md",
"chars": 16134,
"preview": "# stash-box\n\n[\nRUN apt-get update \\\n && apt-get install -y "
},
{
"path": "frontend/.gitattributes",
"chars": 25,
"preview": "src/**/*.ts* text eol=lf\n"
},
{
"path": "frontend/.gitignore",
"chars": 112,
"preview": ".idea/\ndist/\nnode_modules/\nsrc/**/*.jsx\ntests/__coverage__/\ntests/**/*.jsx\n.eslintcache\n.env*.local\nbuild\n*.swp\n"
},
{
"path": "frontend/.nvmrc",
"chars": 3,
"preview": "24\n"
},
{
"path": "frontend/README.md",
"chars": 1624,
"preview": "# Stash-box frontend\n\nThis project builds the frontend for the stash-box server. It can be used to build the static bund"
},
{
"path": "frontend/biome.json",
"chars": 660,
"preview": "{\n \"$schema\": \"https://biomejs.dev/schemas/2.2.0/schema.json\",\n \"vcs\": { \"enabled\": false, \"clientKind\": \"git\", \"useIg"
},
{
"path": "frontend/codegen.yml",
"chars": 499,
"preview": "overwrite: true\nschema: \"../graphql/schema/**/*.graphql\"\ndocuments: \"src/graphql/**/*.gql\"\ngenerates:\n src/graphql/type"
},
{
"path": "frontend/embed.go",
"chars": 67,
"preview": "package frontend\n\nimport \"embed\"\n\n//go:embed build\nvar FS embed.FS\n"
},
{
"path": "frontend/index.html",
"chars": 479,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta\n name=\"viewport\"\n content=\"wi"
},
{
"path": "frontend/package.json",
"chars": 2626,
"preview": "{\n \"name\": \"stash-box-frontend\",\n \"version\": \"0.1.0\",\n \"description\": \"Stash-box\",\n \"license\": \"MIT\",\n \"scripts\": {"
},
{
"path": "frontend/src/App.scss",
"chars": 2303,
"preview": "@import \"./styles/theme\";\n@import \"./hooks/toast\";\n@import \"./pages/home/styles\";\n@import \"./pages/users/styles\";\n@impor"
},
{
"path": "frontend/src/App.tsx",
"chars": 996,
"preview": "import type { FC } from \"react\";\nimport { ApolloProvider } from \"@apollo/client/react\";\nimport { BrowserRouter, Route, R"
},
{
"path": "frontend/src/Login.tsx",
"chars": 3862,
"preview": "import { type FC, useState } from \"react\";\nimport { Link, useLocation, useNavigate } from \"react-router-dom\";\nimport { u"
},
{
"path": "frontend/src/Main.tsx",
"chars": 5459,
"preview": "import { type FC, useEffect } from \"react\";\nimport { Navbar, Nav, Button, Badge } from \"react-bootstrap\";\nimport { NavLi"
},
{
"path": "frontend/src/components/amendableEditCard/AmendableChangeRow.tsx",
"chars": 1858,
"preview": "import type { FC } from \"react\";\nimport { Col, Row, Button } from \"react-bootstrap\";\nimport { faXmark, faUndo } from \"@f"
},
{
"path": "frontend/src/components/amendableEditCard/AmendableImageChangeRow.tsx",
"chars": 5552,
"preview": "import type { FC } from \"react\";\nimport { Col, Row, Button } from \"react-bootstrap\";\nimport { faXmark, faUndo } from \"@f"
},
{
"path": "frontend/src/components/amendableEditCard/AmendableLinkedChangeRow.tsx",
"chars": 2297,
"preview": "import type { FC } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Col, Row, Button } from \"react-bootst"
},
{
"path": "frontend/src/components/amendableEditCard/AmendableListChangeRow.tsx",
"chars": 4895,
"preview": "import type { PropsWithChildren } from \"react\";\nimport { Col, Row, Button } from \"react-bootstrap\";\nimport { faXmark, fa"
},
{
"path": "frontend/src/components/amendableEditCard/AmendableModifyEdit.tsx",
"chars": 17189,
"preview": "import type { FC } from \"react\";\nimport { Col, Row, Button } from \"react-bootstrap\";\nimport {\n faCheck,\n faXmark,\n fa"
},
{
"path": "frontend/src/components/amendableEditCard/AmendableURLChangeRow.tsx",
"chars": 5342,
"preview": "import type { FC } from \"react\";\nimport { Col, Row, Button } from \"react-bootstrap\";\nimport { faXmark, faUndo } from \"@f"
},
{
"path": "frontend/src/components/amendableEditCard/AmendmentContext.tsx",
"chars": 3855,
"preview": "import {\n createContext,\n useContext,\n useState,\n useCallback,\n type FC,\n type ReactNode,\n} from \"react\";\n\nexport "
},
{
"path": "frontend/src/components/amendableEditCard/index.ts",
"chars": 556,
"preview": "export { default as AmendableModifyEdit } from \"./AmendableModifyEdit\";\nexport { default as AmendableChangeRow } from \"."
},
{
"path": "frontend/src/components/changeRow/ChangeRow.tsx",
"chars": 804,
"preview": "import type { FC } from \"react\";\nimport { Col, Row } from \"react-bootstrap\";\nimport cx from \"classnames\";\n\nexport interf"
},
{
"path": "frontend/src/components/changeRow/index.ts",
"chars": 39,
"preview": "export { default } from \"./ChangeRow\";\n"
},
{
"path": "frontend/src/components/checkboxSelect/CheckboxSelect.tsx",
"chars": 2476,
"preview": "// biome-ignore-all lint/correctness/noNestedComponentDefinitions: Necessary for react-select\nimport type { FC } from \"r"
},
{
"path": "frontend/src/components/checkboxSelect/index.ts",
"chars": 79,
"preview": "import CheckboxSelect from \"./CheckboxSelect\";\n\nexport default CheckboxSelect;\n"
},
{
"path": "frontend/src/components/checkboxSelect/styles.scss",
"chars": 36,
"preview": ".CheckboxSelect {\n width: 20rem;\n}\n"
},
{
"path": "frontend/src/components/deleteButton/DeleteButton.tsx",
"chars": 1183,
"preview": "import { type FC, useState } from \"react\";\nimport { Button } from \"react-bootstrap\";\n\nimport Modal from \"src/components/"
},
{
"path": "frontend/src/components/deleteButton/index.ts",
"chars": 73,
"preview": "import DeleteButton from \"./DeleteButton\";\n\nexport default DeleteButton;\n"
},
{
"path": "frontend/src/components/editCard/AddComment.tsx",
"chars": 2096,
"preview": "import { type FC, useState } from \"react\";\nimport { Button, Form } from \"react-bootstrap\";\nimport { useEditComment } fro"
},
{
"path": "frontend/src/components/editCard/EditCard.tsx",
"chars": 3797,
"preview": "import type { FC } from \"react\";\nimport { Card, Col, Row } from \"react-bootstrap\";\nimport { Link } from \"react-router-do"
},
{
"path": "frontend/src/components/editCard/EditComment.tsx",
"chars": 839,
"preview": "import type { FC } from \"react\";\nimport { Card } from \"react-bootstrap\";\nimport { Link } from \"react-router-dom\";\n\nimpor"
},
{
"path": "frontend/src/components/editCard/EditExpiration.tsx",
"chars": 1883,
"preview": "import type { FC } from \"react\";\nimport type { Temporal } from \"temporal-polyfill\";\nimport {\n formatDistance,\n parseIn"
},
{
"path": "frontend/src/components/editCard/EditHeader.tsx",
"chars": 5053,
"preview": "import { type FC, useMemo } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Col, Row } from \"react-boots"
},
{
"path": "frontend/src/components/editCard/EditStatus.tsx",
"chars": 1760,
"preview": "import type { FC } from \"react\";\nimport { Badge, type BadgeProps } from \"react-bootstrap\";\n\nimport { VoteStatusEnum } fr"
},
{
"path": "frontend/src/components/editCard/ModifyEdit.tsx",
"chars": 16673,
"preview": "import type { FC } from \"react\";\nimport { Col, Row } from \"react-bootstrap\";\nimport { faCheck, faXmark, faEdit } from \"@"
},
{
"path": "frontend/src/components/editCard/VoteBar.tsx",
"chars": 3631,
"preview": "import { type FC, useState } from \"react\";\nimport { Button, Form } from \"react-bootstrap\";\nimport { faCheck } from \"@for"
},
{
"path": "frontend/src/components/editCard/Votes.tsx",
"chars": 1369,
"preview": "import type { FC } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { sortBy } from \"lodash-es\";\n\nimport { "
},
{
"path": "frontend/src/components/editCard/index.ts",
"chars": 61,
"preview": "import EditCard from \"./EditCard\";\n\nexport default EditCard;\n"
},
{
"path": "frontend/src/components/editCard/renderEntity.tsx",
"chars": 1403,
"preview": "import { Link } from \"react-router-dom\";\n\nimport type { FingerprintAlgorithm, PerformerFragment } from \"src/graphql\";\n\ni"
},
{
"path": "frontend/src/components/editCard/styles.scss",
"chars": 1121,
"preview": ".EditComment {\n background-color: $textfield-bg;\n margin-top: 1rem;\n\n .card-body a {\n color: $link-color;\n }\n\n b"
},
{
"path": "frontend/src/components/editImages/editImages.tsx",
"chars": 5238,
"preview": "import { type FC, type ChangeEvent, useState } from \"react\";\nimport { Button, Col, Form, Row } from \"react-bootstrap\";\ni"
},
{
"path": "frontend/src/components/editImages/index.ts",
"chars": 67,
"preview": "import EditImages from \"./editImages\";\n\nexport default EditImages;\n"
},
{
"path": "frontend/src/components/editImages/styles.scss",
"chars": 1260,
"preview": ".EditImages {\n &-drop {\n align-items: center;\n border: 3px dashed $text-color;\n border-radius: 10px;\n displ"
},
{
"path": "frontend/src/components/form/BodyModification.tsx",
"chars": 2814,
"preview": "// biome-ignore-all lint/correctness/noNestedComponentDefinitions: react-select\nimport type { FC, ChangeEvent } from \"re"
},
{
"path": "frontend/src/components/form/EditNote.tsx",
"chars": 868,
"preview": "import type { FC } from \"react\";\nimport { Form } from \"react-bootstrap\";\nimport cx from \"classnames\";\nimport type { Fiel"
},
{
"path": "frontend/src/components/form/Image.tsx",
"chars": 875,
"preview": "import type { FC } from \"react\";\nimport { Button } from \"react-bootstrap\";\nimport { faXmark } from \"@fortawesome/free-so"
},
{
"path": "frontend/src/components/form/NavButtons.tsx",
"chars": 606,
"preview": "import type { FC } from \"react\";\nimport { Button } from \"react-bootstrap\";\nimport { useNavigate } from \"react-router-dom"
},
{
"path": "frontend/src/components/form/NoteInput.tsx",
"chars": 1581,
"preview": "import { type FC, type ChangeEvent, useState } from \"react\";\nimport { Form, Tabs, Tab } from \"react-bootstrap\";\nimport c"
},
{
"path": "frontend/src/components/form/SubmitButtons.tsx",
"chars": 644,
"preview": "import type { FC } from \"react\";\nimport { Button } from \"react-bootstrap\";\nimport { useNavigate } from \"react-router-dom"
},
{
"path": "frontend/src/components/form/index.ts",
"chars": 275,
"preview": "export { default as BodyModification } from \"./BodyModification\";\nexport { default as Image } from \"./Image\";\nexport { d"
},
{
"path": "frontend/src/components/form/styles.scss",
"chars": 971,
"preview": ".BodyModification {\n &-remove {\n z-index: 2;\n background: transparent;\n border: none;\n color: rgba(0 0 0 / "
},
{
"path": "frontend/src/components/fragments/ErrorMessage.tsx",
"chars": 286,
"preview": "import type { FC, ReactNode } from \"react\";\n\ninterface IProps {\n error: string | ReactNode;\n}\n\nconst ErrorMessage: FC<I"
},
{
"path": "frontend/src/components/fragments/Favorite.tsx",
"chars": 1480,
"preview": "import type { FC, MouseEvent } from \"react\";\nimport { faStar } from \"@fortawesome/free-solid-svg-icons\";\nimport { faStar"
},
{
"path": "frontend/src/components/fragments/GenderIcon.tsx",
"chars": 679,
"preview": "import type { FC } from \"react\";\nimport {\n faVenus,\n faTransgender,\n faMars,\n faVenusMars,\n} from \"@fortawesome/free"
},
{
"path": "frontend/src/components/fragments/Help.tsx",
"chars": 700,
"preview": "import type { FC } from \"react\";\nimport { Button, OverlayTrigger, Popover } from \"react-bootstrap\";\nimport { Icon } from"
},
{
"path": "frontend/src/components/fragments/Icon.tsx",
"chars": 610,
"preview": "import type { FC } from \"react\";\nimport cx from \"classnames\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontaw"
},
{
"path": "frontend/src/components/fragments/LoadingIndicator.tsx",
"chars": 981,
"preview": "import { type FC, useEffect, useState } from \"react\";\nimport { Spinner } from \"react-bootstrap\";\nimport cx from \"classna"
},
{
"path": "frontend/src/components/fragments/PerformerName.tsx",
"chars": 994,
"preview": "import type { FC } from \"react\";\nimport type { PerformerFragment } from \"src/graphql\";\n\ninterface PerformerNameProps {\n "
},
{
"path": "frontend/src/components/fragments/SearchHint.tsx",
"chars": 401,
"preview": "import type React from \"react\";\nimport { faCircleQuestion } from \"@fortawesome/free-regular-svg-icons\";\nimport { Icon, T"
},
{
"path": "frontend/src/components/fragments/SearchInput.tsx",
"chars": 647,
"preview": "import { components } from \"react-select\";\nimport { extractIdFromUrl } from \"src/utils\";\n\n// Shared Input component for "
},
{
"path": "frontend/src/components/fragments/SiteLink.tsx",
"chars": 881,
"preview": "import type { FC } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport cx from \"classnames\";\nimport { siteHref"
},
{
"path": "frontend/src/components/fragments/TagLink.tsx",
"chars": 863,
"preview": "import type { FC } from \"react\";\nimport { Badge, Button } from \"react-bootstrap\";\nimport { Link } from \"react-router-dom"
},
{
"path": "frontend/src/components/fragments/Thumbnail.tsx",
"chars": 882,
"preview": "import type { FC } from \"react\";\nimport cx from \"classnames\";\nimport { faXmark } from \"@fortawesome/free-solid-svg-icons"
},
{
"path": "frontend/src/components/fragments/Tooltip.tsx",
"chars": 706,
"preview": "import type { FC, ReactElement } from \"react\";\nimport {\n OverlayTrigger,\n Tooltip as BSTooltip,\n type PopoverProps,\n}"
},
{
"path": "frontend/src/components/fragments/index.ts",
"chars": 640,
"preview": "export { default as GenderIcon } from \"./GenderIcon\";\nexport { default as LoadingIndicator } from \"./LoadingIndicator\";\n"
},
{
"path": "frontend/src/components/fragments/styles.scss",
"chars": 1937,
"preview": "@keyframes fade-in {\n 0% {\n opacity: 0;\n }\n\n 100% {\n opacity: 1;\n }\n}\n\n.LoadingIndicator {\n align-items: cent"
},
{
"path": "frontend/src/components/image/Image.tsx",
"chars": 2151,
"preview": "import { type FC, useState } from \"react\";\nimport cx from \"classnames\";\nimport { faXmark } from \"@fortawesome/free-solid"
},
{
"path": "frontend/src/components/image/index.ts",
"chars": 35,
"preview": "export { default } from \"./Image\";\n"
},
{
"path": "frontend/src/components/image/styles.scss",
"chars": 661,
"preview": ".Image {\n position: relative;\n display: flex;\n max-height: 100%;\n max-width: 100%;\n\n &-image {\n max-width: 100%;"
},
{
"path": "frontend/src/components/imageCarousel/ImageCarousel.tsx",
"chars": 1918,
"preview": "import { type FC, useState } from \"react\";\nimport { Button } from \"react-bootstrap\";\nimport {\n faChevronLeft,\n faChevr"
},
{
"path": "frontend/src/components/imageCarousel/index.ts",
"chars": 76,
"preview": "import ImageCarousel from \"./ImageCarousel\";\n\nexport default ImageCarousel;\n"
},
{
"path": "frontend/src/components/imageCarousel/styles.scss",
"chars": 1151,
"preview": ".image-carousel {\n height: 100%;\n width: 100%;\n\n .image-container {\n align-items: center;\n display: flex;\n f"
},
{
"path": "frontend/src/components/imageChangeRow/ImageChangeRow.tsx",
"chars": 1952,
"preview": "import type { FC } from \"react\";\nimport { Col, Row } from \"react-bootstrap\";\nimport ImageComponent from \"src/components/"
},
{
"path": "frontend/src/components/imageChangeRow/index.ts",
"chars": 44,
"preview": "export { default } from \"./ImageChangeRow\";\n"
},
{
"path": "frontend/src/components/imageChangeRow/styles.scss",
"chars": 125,
"preview": ".ImageChangeRow {\n display: flex;\n flex-wrap: wrap;\n\n .Image {\n height: 150px;\n margin: 5px;\n width: auto;\n "
},
{
"path": "frontend/src/components/linkedChangeRow/LinkedChangeRow.tsx",
"chars": 1270,
"preview": "import type { FC } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Col, Row } from \"react-bootstrap\";\nim"
},
{
"path": "frontend/src/components/linkedChangeRow/index.ts",
"chars": 45,
"preview": "export { default } from \"./LinkedChangeRow\";\n"
},
{
"path": "frontend/src/components/list/EditList.tsx",
"chars": 2703,
"preview": "import type { FC } from \"react\";\n\nimport { useEditFilter, usePagination } from \"src/hooks\";\nimport {\n useEdits,\n type "
},
{
"path": "frontend/src/components/list/List.tsx",
"chars": 1597,
"preview": "import { type FC, type ReactNode, useEffect, useState } from \"react\";\nimport { LoadingIndicator } from \"src/components/f"
},
{
"path": "frontend/src/components/list/SceneList.tsx",
"chars": 5095,
"preview": "import type { FC } from \"react\";\nimport Select from \"react-select\";\nimport { Button, Col, Form, InputGroup, Row } from \""
},
{
"path": "frontend/src/components/list/TagList.tsx",
"chars": 2365,
"preview": "import type { FC } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Card, Form, Row } from \"react-bootstr"
},
{
"path": "frontend/src/components/list/URLList.tsx",
"chars": 545,
"preview": "import type { FC } from \"react\";\nimport { SiteLink } from \"src/components/fragments\";\n\ninterface URLListProps {\n urls: "
},
{
"path": "frontend/src/components/list/index.ts",
"chars": 240,
"preview": "export { default as List } from \"./List\";\nexport { default as SceneList } from \"./SceneList\";\nexport { default as TagLis"
},
{
"path": "frontend/src/components/list/styles.scss",
"chars": 151,
"preview": ".URLList {\n list-style-type: none;\n padding: 0;\n\n img {\n width: 16px;\n }\n}\n\n.FavoriteFilter {\n width: 240px;\n}\n\n"
},
{
"path": "frontend/src/components/listChangeRow/ListChangeRow.tsx",
"chars": 1562,
"preview": "import type { PropsWithChildren } from \"react\";\n\nimport { Col, Row } from \"react-bootstrap\";\n\ninterface ListChangeRowPro"
},
{
"path": "frontend/src/components/listChangeRow/index.ts",
"chars": 43,
"preview": "export { default } from \"./ListChangeRow\";\n"
},
{
"path": "frontend/src/components/modal/Modal.tsx",
"chars": 1102,
"preview": "import type { ReactNode, FC } from \"react\";\nimport { Modal, Button } from \"react-bootstrap\";\n\ninterface ModalProps {\n c"
},
{
"path": "frontend/src/components/modal/index.ts",
"chars": 52,
"preview": "import Modal from \"./Modal\";\n\nexport default Modal;\n"
},
{
"path": "frontend/src/components/multiSelect/MultiSelect.tsx",
"chars": 1681,
"preview": "// biome-ignore-all lint/correctness/noNestedComponentDefinitions: react-select\nimport type { FC } from \"react\";\nimport "
},
{
"path": "frontend/src/components/multiSelect/index.ts",
"chars": 70,
"preview": "import MultiSelect from \"./MultiSelect\";\n\nexport default MultiSelect;\n"
},
{
"path": "frontend/src/components/multiSelect/styles.scss",
"chars": 204,
"preview": ".TagSelect {\n margin-top: 0.5rem;\n\n &-list {\n margin-bottom: 1rem;\n }\n\n &-container {\n display: flex;\n }\n\n &"
},
{
"path": "frontend/src/components/pagination/Pagination.tsx",
"chars": 2045,
"preview": "import type { FC, MouseEvent } from \"react\";\nimport { Pagination } from \"react-bootstrap\";\n\ninterface PaginationProps {\n"
},
{
"path": "frontend/src/components/pagination/index.ts",
"chars": 67,
"preview": "import Pagination from \"./Pagination\";\n\nexport default Pagination;\n"
},
{
"path": "frontend/src/components/performerCard/PerformerCard.tsx",
"chars": 1433,
"preview": "import type { FC } from \"react\";\nimport { Card } from \"react-bootstrap\";\nimport { Link } from \"react-router-dom\";\nimport"
},
{
"path": "frontend/src/components/performerCard/index.ts",
"chars": 76,
"preview": "import PerformerCard from \"./PerformerCard\";\n\nexport default PerformerCard;\n"
},
{
"path": "frontend/src/components/performerCard/styles.scss",
"chars": 386,
"preview": ".PerformerCard {\n &-image {\n align-items: center;\n display: flex;\n aspect-ratio: 2 / 3;\n\n img {\n heigh"
},
{
"path": "frontend/src/components/performerSelect/PerformerSelect.tsx",
"chars": 2076,
"preview": "import { type FC, useState } from \"react\";\n\nimport { TagLink } from \"src/components/fragments\";\nimport SearchField, { Se"
},
{
"path": "frontend/src/components/performerSelect/index.ts",
"chars": 82,
"preview": "import PerformerSelect from \"./PerformerSelect\";\n\nexport default PerformerSelect;\n"
},
{
"path": "frontend/src/components/performerSelect/styles.scss",
"chars": 207,
"preview": ".PerformerSelect {\n margin-top: 0.5rem;\n\n &-list {\n margin: 0.5rem 0;\n }\n\n &-container {\n display: flex;\n }\n\n"
},
{
"path": "frontend/src/components/sceneCard/SceneCard.tsx",
"chars": 1894,
"preview": "import type { FC } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Card } from \"react-bootstrap\";\nimport"
},
{
"path": "frontend/src/components/sceneCard/index.ts",
"chars": 64,
"preview": "import SceneCard from \"./SceneCard\";\n\nexport default SceneCard;\n"
},
{
"path": "frontend/src/components/sceneCard/styles.scss",
"chars": 668,
"preview": ".SceneCard {\n &.card {\n box-shadow: none;\n border-radius: 0;\n background-color: transparent;\n }\n\n &-body {\n "
},
{
"path": "frontend/src/components/searchField/SearchField.tsx",
"chars": 5138,
"preview": "import { type FC, type KeyboardEvent, useRef, useState } from \"react\";\nimport { useApolloClient } from \"@apollo/client/r"
},
{
"path": "frontend/src/components/searchField/handleResult.ts",
"chars": 5879,
"preview": "import type { SearchAllQuery, SearchPerformersQuery } from \"src/graphql\";\nimport { filterData, formatDisambiguation } fr"
},
{
"path": "frontend/src/components/searchField/index.ts",
"chars": 72,
"preview": "export { default } from \"./SearchField\";\nexport * from \"./SearchField\";\n"
},
{
"path": "frontend/src/components/searchField/styles.scss",
"chars": 341,
"preview": ".SearchField {\n width: 400px;\n z-index: 5;\n\n .search-value {\n font-size: 14px;\n font-weight: 500;\n }\n\n .searc"
},
{
"path": "frontend/src/components/studioSelect/StudioSelect.tsx",
"chars": 4154,
"preview": "import type { FC } from \"react\";\nimport { components } from \"react-select\";\nimport Async from \"react-select/async\";\nimpo"
},
{
"path": "frontend/src/components/studioSelect/index.ts",
"chars": 42,
"preview": "export { default } from \"./StudioSelect\";\n"
},
{
"path": "frontend/src/components/studioSelect/styles.scss",
"chars": 164,
"preview": ".StudioSelect {\n .parent-studio {\n color: $text-muted;\n }\n\n .react-select__value-container {\n .parent-studio {\n"
},
{
"path": "frontend/src/components/tagFilter/TagFilter.tsx",
"chars": 3508,
"preview": "import type { FC } from \"react\";\nimport Async from \"react-select/async\";\nimport type { OnChangeValue, MenuPlacement } fr"
},
{
"path": "frontend/src/components/tagFilter/index.ts",
"chars": 64,
"preview": "import TagFilter from \"./TagFilter\";\n\nexport default TagFilter;\n"
},
{
"path": "frontend/src/components/tagFilter/styles.scss",
"chars": 297,
"preview": ".TagFilter {\n &-select {\n display: inline-block;\n margin-right: 0.5rem;\n width: 14rem;\n\n .react-select__men"
},
{
"path": "frontend/src/components/tagSelect/TagSelect.tsx",
"chars": 4628,
"preview": "import { type FC, useState } from \"react\";\nimport Async from \"react-select/async\";\nimport type { OnChangeValue, MenuPlac"
},
{
"path": "frontend/src/components/tagSelect/index.ts",
"chars": 64,
"preview": "import TagSelect from \"./TagSelect\";\n\nexport default TagSelect;\n"
},
{
"path": "frontend/src/components/tagSelect/styles.scss",
"chars": 345,
"preview": ".TagSelect {\n margin-top: 0.5rem;\n\n &-list {\n margin-bottom: 1rem;\n }\n\n &-container {\n display: flex;\n }\n\n &"
},
{
"path": "frontend/src/components/title/Title.tsx",
"chars": 433,
"preview": "import type { FC } from \"react\";\nimport { Helmet } from \"react-helmet\";\n\n// Title is only injected in production, so def"
},
{
"path": "frontend/src/components/title/index.ts",
"chars": 35,
"preview": "export { default } from \"./Title\";\n"
},
{
"path": "frontend/src/components/urlChangeRow/URLChangeRow.tsx",
"chars": 1616,
"preview": "import type { FC } from \"react\";\nimport { Col, Row } from \"react-bootstrap\";\nimport { SiteLink } from \"src/components/fr"
},
{
"path": "frontend/src/components/urlChangeRow/index.ts",
"chars": 85,
"preview": "export { default } from \"./URLChangeRow\";\nexport type { URL } from \"./URLChangeRow\";\n"
},
{
"path": "frontend/src/components/urlInput/index.ts",
"chars": 61,
"preview": "import URLInput from \"./urlInput\";\n\nexport default URLInput;\n"
},
{
"path": "frontend/src/components/urlInput/styles.scss",
"chars": 173,
"preview": ".URLInput {\n width: 100%;\n\n ul {\n list-style-type: none;\n padding-left: 0;\n }\n\n li {\n margin-bottom: 0.5rem"
},
{
"path": "frontend/src/components/urlInput/urlInput.tsx",
"chars": 4927,
"preview": "import { type FC, useRef, useState } from \"react\";\nimport { Button, Form, InputGroup } from \"react-bootstrap\";\nimport { "
},
{
"path": "frontend/src/constants/enums.ts",
"chars": 3566,
"preview": "import {\n BreastTypeEnum,\n EthnicityEnum,\n EthnicityFilterEnum,\n EyeColorEnum,\n HairColorEnum,\n GenderEnum,\n Gend"
},
{
"path": "frontend/src/constants/index.ts",
"chars": 50,
"preview": "export * from \"./enums\";\nexport * from \"./route\";\n"
},
{
"path": "frontend/src/constants/route.ts",
"chars": 2751,
"preview": "export const ROUTE_HOME = \"/\";\nexport const ROUTE_LOGIN = \"/login\";\nexport const ROUTE_LOGOUT = \"/logout\";\nexport const "
},
{
"path": "frontend/src/context.tsx",
"chars": 425,
"preview": "import { createContext, useContext } from \"react\";\n\nimport type { RoleEnum } from \"src/graphql\";\n\nexport interface User "
},
{
"path": "frontend/src/graphql/fragments/CommentFragment.gql",
"chars": 95,
"preview": "fragment CommentFragment on EditComment {\n id\n user {\n id\n name\n }\n date\n comment\n}\n"
},
{
"path": "frontend/src/graphql/fragments/EditFragment.gql",
"chars": 4736,
"preview": "#import \"../fragments/PerformerFragment.gql\"\n#import \"../fragments/StudioFragment.gql\"\n#import \"../fragments/ImageFragme"
},
{
"path": "frontend/src/graphql/fragments/FingerprintFragment.gql",
"chars": 78,
"preview": "fragment FingerprintFragment on Fingerprint {\n hash\n algorithm\n duration\n}\n"
},
{
"path": "frontend/src/graphql/fragments/ImageFragment.gql",
"chars": 64,
"preview": "fragment ImageFragment on Image {\n id\n url\n width\n height\n}\n"
},
{
"path": "frontend/src/graphql/fragments/PerformerFragment.gql",
"chars": 560,
"preview": "#import \"../fragments/ImageFragment.gql\"\n#import \"../fragments/URLFragment.gql\"\nfragment PerformerFragment on Performer "
},
{
"path": "frontend/src/graphql/fragments/QuerySceneFragment.gql",
"chars": 389,
"preview": "#import \"../fragments/URLFragment.gql\"\n#import \"../fragments/ImageFragment.gql\"\n#import \"../fragments/ScenePerformerFrag"
},
{
"path": "frontend/src/graphql/fragments/SceneFragment.gql",
"chars": 682,
"preview": "#import \"../fragments/ImageFragment.gql\"\n#import \"../fragments/ScenePerformerFragment.gql\"\n#import \"../fragments/URLFrag"
},
{
"path": "frontend/src/graphql/fragments/ScenePerformerFragment.gql",
"chars": 107,
"preview": "fragment ScenePerformerFragment on Performer {\n id\n name\n disambiguation\n deleted\n gender\n aliases\n}\n"
},
{
"path": "frontend/src/graphql/fragments/SearchPerformerFragment.gql",
"chars": 345,
"preview": "#import \"../fragments/ImageFragment.gql\"\n#import \"../fragments/URLFragment.gql\"\nfragment SearchPerformerFragment on Perf"
},
{
"path": "frontend/src/graphql/fragments/StudioFragment.gql",
"chars": 237,
"preview": "#import \"../fragments/URLFragment.gql\"\nfragment StudioFragment on Studio {\n id\n name\n aliases\n parent {\n id\n n"
},
{
"path": "frontend/src/graphql/fragments/TagFragment.gql",
"chars": 111,
"preview": "fragment TagFragment on Tag {\n id\n name\n description\n deleted\n category {\n id\n name\n }\n aliases\n}\n"
},
{
"path": "frontend/src/graphql/fragments/URLFragment.gql",
"chars": 76,
"preview": "fragment URLFragment on URL {\n url\n site {\n id\n name\n icon\n }\n}\n"
},
{
"path": "frontend/src/graphql/index.ts",
"chars": 81,
"preview": "export * from \"./queries\";\nexport * from \"./mutations\";\nexport * from \"./types\";\n"
},
{
"path": "frontend/src/graphql/mutations/ActivateNewUser.gql",
"chars": 106,
"preview": "mutation ActivateNewUser($input: ActivateNewUserInput!) {\n activateNewUser(input: $input) {\n id\n }\n}\n"
},
{
"path": "frontend/src/graphql/mutations/AddImage.gql",
"chars": 128,
"preview": "mutation AddImage($imageData: ImageCreateInput!) {\n imageCreate(input: $imageData) {\n id\n url\n width\n heigh"
},
{
"path": "frontend/src/graphql/mutations/AddScene.gql",
"chars": 517,
"preview": "mutation AddScene($sceneData: SceneCreateInput!) {\n sceneCreate(input: $sceneData) {\n id\n release_date\n produc"
},
{
"path": "frontend/src/graphql/mutations/AddSite.gql",
"chars": 153,
"preview": "mutation AddSite($siteData: SiteCreateInput!) {\n siteCreate(input: $siteData) {\n id\n name\n description\n url"
},
{
"path": "frontend/src/graphql/mutations/AddStudio.gql",
"chars": 197,
"preview": "mutation AddStudio($studioData: StudioCreateInput!) {\n studioCreate(input: $studioData) {\n id\n name\n aliases\n "
},
{
"path": "frontend/src/graphql/mutations/AddTagCategory.gql",
"chars": 158,
"preview": "mutation AddTagCategory($categoryData: TagCategoryCreateInput!) {\n tagCategoryCreate(input: $categoryData) {\n id\n "
},
{
"path": "frontend/src/graphql/mutations/AddUser.gql",
"chars": 123,
"preview": "mutation AddUser($userData: UserCreateInput!) {\n userCreate(input: $userData) {\n id\n name\n email\n roles\n }"
},
{
"path": "frontend/src/graphql/mutations/AmendEdit.gql",
"chars": 101,
"preview": "mutation AmendEdit($input: AmendEditInput!) {\n amendEdit(input: $input) {\n ...EditFragment\n }\n}\n"
},
{
"path": "frontend/src/graphql/mutations/ApplyEdit.gql",
"chars": 141,
"preview": "#import \"../fragments/EditFragment.gql\"\nmutation ApplyEdit($input: ApplyEditInput!) {\n applyEdit(input: $input) {\n ."
},
{
"path": "frontend/src/graphql/mutations/CancelEdit.gql",
"chars": 526,
"preview": "mutation CancelEdit($input: CancelEditInput!) {\n cancelEdit(input: $input) {\n id\n target_type\n operation\n s"
},
{
"path": "frontend/src/graphql/mutations/ChangePassword.gql",
"chars": 100,
"preview": "mutation ChangePassword($userData: UserChangePasswordInput!) {\n changePassword(input: $userData)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/ConfirmChangeEmail.gql",
"chars": 81,
"preview": "mutation ConfirmChangeEmail($token: ID!) {\n confirmChangeEmail(token: $token)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/DeleteDraft.gql",
"chars": 59,
"preview": "mutation DeleteDraft($id: ID!) {\n destroyDraft(id: $id)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/DeleteEdit.gql",
"chars": 78,
"preview": "mutation DeleteEdit($input: DeleteEditInput!) {\n deleteEdit(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/DeleteFingerprintSubmissions.gql",
"chars": 141,
"preview": "mutation DeleteFingerprintSubmissions(\n $input: DeleteFingerprintSubmissionsInput!\n) {\n sceneDeleteFingerprintSubmissi"
},
{
"path": "frontend/src/graphql/mutations/DeleteScene.gql",
"chars": 83,
"preview": "mutation DeleteScene($input: SceneDestroyInput!) {\n sceneDestroy(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/DeleteSite.gql",
"chars": 80,
"preview": "mutation DeleteSite($input: SiteDestroyInput!) {\n siteDestroy(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/DeleteStudio.gql",
"chars": 86,
"preview": "mutation DeleteStudio($input: StudioDestroyInput!) {\n studioDestroy(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/DeleteTagCategory.gql",
"chars": 101,
"preview": "mutation DeleteTagCategory($input: TagCategoryDestroyInput!) {\n tagCategoryDestroy(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/DeleteUser.gql",
"chars": 80,
"preview": "mutation DeleteUser($input: UserDestroyInput!) {\n userDestroy(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/EditComment.gql",
"chars": 183,
"preview": "#import \"../fragments/CommentFragment.gql\"\nmutation EditComment($input: EditCommentInput!) {\n editComment(input: $input"
},
{
"path": "frontend/src/graphql/mutations/FavoritePerformer.gql",
"chars": 112,
"preview": "mutation FavoritePerformer($id: ID!, $favorite: Boolean!) {\n favoritePerformer(id: $id, favorite: $favorite)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/FavoriteStudio.gql",
"chars": 106,
"preview": "mutation FavoriteStudio($id: ID!, $favorite: Boolean!) {\n favoriteStudio(id: $id, favorite: $favorite)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/GenerateInviteCode.gql",
"chars": 103,
"preview": "mutation GenerateInviteCodes($input: GenerateInviteCodeInput) {\n generateInviteCodes(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/GrantInvite.gql",
"chars": 81,
"preview": "mutation GrantInvite($input: GrantInviteInput!) {\n grantInvite(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/MarkNotificationRead.gql",
"chars": 130,
"preview": "mutation MarkNotificationRead($notification: MarkNotificationReadInput!) {\n markNotificationsRead(notification: $notifi"
},
{
"path": "frontend/src/graphql/mutations/MarkNotificationsRead.gql",
"chars": 59,
"preview": "mutation MarkNotificationsRead {\n markNotificationsRead\n}\n"
},
{
"path": "frontend/src/graphql/mutations/MoveFingerprintSubmissions.gql",
"chars": 131,
"preview": "mutation MoveFingerprintSubmissions($input: MoveFingerprintSubmissionsInput!) {\n sceneMoveFingerprintSubmissions(input:"
},
{
"path": "frontend/src/graphql/mutations/NewUser.gql",
"chars": 69,
"preview": "mutation NewUser($input: NewUserInput!) {\n newUser(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/PerformerEdit.gql",
"chars": 169,
"preview": "#import \"../fragments/EditFragment.gql\"\nmutation PerformerEdit($performerData: PerformerEditInput!) {\n performerEdit(in"
},
{
"path": "frontend/src/graphql/mutations/PerformerEditUpdate.gql",
"chars": 200,
"preview": "#import \"../fragments/EditFragment.gql\"\nmutation PerformerEditUpdate($id: ID!, $performerData: PerformerEditInput!) {\n "
},
{
"path": "frontend/src/graphql/mutations/RegenerateAPIKey.gql",
"chars": 81,
"preview": "mutation RegenerateAPIKey($user_id: ID) {\n regenerateAPIKey(userID: $user_id)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/RequestChangeEmail.gql",
"chars": 53,
"preview": "mutation RequestChangeEmail {\n requestChangeEmail\n}\n"
},
{
"path": "frontend/src/graphql/mutations/RescindInviteCode.gql",
"chars": 76,
"preview": "mutation RescindInviteCode($code: ID!) {\n rescindInviteCode(code: $code)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/ResetPassword.gql",
"chars": 87,
"preview": "mutation ResetPassword($input: ResetPasswordInput!) {\n resetPassword(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/RevokeInvite.gql",
"chars": 84,
"preview": "mutation RevokeInvite($input: RevokeInviteInput!) {\n revokeInvite(input: $input)\n}\n"
},
{
"path": "frontend/src/graphql/mutations/SceneEdit.gql",
"chars": 149,
"preview": "#import \"../fragments/EditFragment.gql\"\nmutation SceneEdit($sceneData: SceneEditInput!) {\n sceneEdit(input: $sceneData)"
},
{
"path": "frontend/src/graphql/mutations/SceneEditUpdate.gql",
"chars": 180,
"preview": "#import \"../fragments/EditFragment.gql\"\nmutation SceneEditUpdate($id: ID!, $sceneData: SceneEditInput!) {\n sceneEditUpd"
}
]
// ... and 552 more files (download for full content)
About this extraction
This page contains the full source code of the stashapp/stash-box GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 752 files (5.0 MB), approximately 1.3M tokens, and a symbol index with 6216 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.