From d6d9319fc4b452be9e9d98667ca97afcb3a3e01b Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sat, 24 Dec 2022 16:26:52 -0800 Subject: [PATCH] feat: improve project icon changing experience --- .editorconfig | 4 + .gitignore | 6 +- .idea/inspectionProfiles/Project_Default.xml | 58 +++++++++++++ .../internal/projects/HangarProject.java | 73 +++++++++------- .../file/LocalStorageFileService.java | 38 ++++---- .../internal/projects/ProjectService.java | 2 +- .../internal/uploads/ImageService.java | 23 ++--- .../internal/uploads/ProjectFiles.java | 87 +++++++++---------- .../src/pages/[user]/[project]/settings.vue | 42 ++++----- frontend/src/types/generated/icons.d.ts | 1 + frontend/src/types/internal/projects.d.ts | 1 + 11 files changed, 197 insertions(+), 138 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.editorconfig b/.editorconfig index 5d27b83da..e8663840a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,7 @@ trim_trailing_whitespace = true insert_final_newline = true ij_any_block_comment_at_first_column = false ij_any_line_comment_at_first_column = false +ij_any_line_comment_add_space_on_reformat = true ij_any_block_comment_add_space = true max_line_length = 160 ij_visual_guides = 160 @@ -19,8 +20,11 @@ ij_html_quote_style = double [*.java] ij_java_insert_inner_class_imports = false ij_java_use_fq_class_names = false +ij_java_use_single_class_imports = true ij_java_class_count_to_use_import_on_demand = 99999 ij_java_names_count_to_use_import_on_demand = 99999 +ij_java_keep_simple_lambdas_in_one_line = true +ij_java_keep_simple_classes_in_one_line = true ij_java_imports_layout = *,|,$* ij_java_generate_final_locals = true ij_java_generate_final_parameters = true diff --git a/.gitignore b/.gitignore index 4abd0f57b..e066a95c9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ work/ .sts4-cache ### IntelliJ IDEA ### -.idea +.idea/* *.iws *.iml *.ipr @@ -33,3 +33,7 @@ build/ ### VS Code ### .vscode/ + + +### Unignore Inspection Profiles ### +!/.idea/inspectionProfiles diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..7d7336192 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,58 @@ + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/papermc/hangar/model/internal/projects/HangarProject.java b/backend/src/main/java/io/papermc/hangar/model/internal/projects/HangarProject.java index b57465c92..e7460dc79 100644 --- a/backend/src/main/java/io/papermc/hangar/model/internal/projects/HangarProject.java +++ b/backend/src/main/java/io/papermc/hangar/model/internal/projects/HangarProject.java @@ -32,8 +32,9 @@ public class HangarProject extends Project implements Joinable private final Collection pages; private final List pinnedVersions; private final Map mainChannelVersions; + private final boolean customIcon; - public HangarProject(final Project project, final long id, final ProjectOwner owner, final List> members, final String lastVisibilityChangeComment, final String lastVisibilityChangeUserName, final HangarProjectInfo info, final Collection pages, final List pinnedVersions, final Map mainChannelVersions) { + public HangarProject(final Project project, final long id, final ProjectOwner owner, final List> members, final String lastVisibilityChangeComment, final String lastVisibilityChangeUserName, final HangarProjectInfo info, final Collection pages, final List pinnedVersions, final Map mainChannelVersions, final boolean customIcon) { super(project); this.id = id; this.owner = owner; @@ -44,20 +45,21 @@ public class HangarProject extends Project implements Joinable this.pages = pages; this.pinnedVersions = pinnedVersions; this.mainChannelVersions = mainChannelVersions; + this.customIcon = customIcon; } public long getId() { - return id; + return this.id; } @Override public long getProjectId() { - return id; + return this.id; } @Override public ProjectOwner getOwner() { - return owner; + return this.owner; } @Override @@ -67,23 +69,23 @@ public class HangarProject extends Project implements Joinable @Override public List> getMembers() { - return members; + return this.members; } public String getLastVisibilityChangeComment() { - return lastVisibilityChangeComment; + return this.lastVisibilityChangeComment; } public String getLastVisibilityChangeUserName() { - return lastVisibilityChangeUserName; + return this.lastVisibilityChangeUserName; } public HangarProjectInfo getInfo() { - return info; + return this.info; } public Collection getPages() { - return pages; + return this.pages; } public List getPinnedVersions() { @@ -91,7 +93,7 @@ public class HangarProject extends Project implements Joinable } public Map getMainChannelVersions() { - return mainChannelVersions; + return this.mainChannelVersions; } @Override @@ -105,9 +107,14 @@ public class HangarProject extends Project implements Joinable ", info=" + this.info + ", pages=" + this.pages + ", pinnedVersions=" + this.pinnedVersions + + ", customIcon=" + this.customIcon + "} " + super.toString(); } + public boolean isCustomIcon() { + return customIcon; + } + public static class HangarProjectInfo { private final int publicVersions; @@ -125,35 +132,35 @@ public class HangarProject extends Project implements Joinable } public int getPublicVersions() { - return publicVersions; + return this.publicVersions; } @RequiresPermission(NamedPermission.MOD_NOTES_AND_FLAGS) public int getFlagCount() { - return flagCount; + return this.flagCount; } @RequiresPermission(NamedPermission.MOD_NOTES_AND_FLAGS) public int getNoteCount() { - return noteCount; + return this.noteCount; } public long getStarCount() { - return starCount; + return this.starCount; } public long getWatcherCount() { - return watcherCount; + return this.watcherCount; } @Override public String toString() { return "HangarProjectInfo{" + - "publicVersions=" + publicVersions + - ", flagCount=" + flagCount + - ", noteCount=" + noteCount + - ", starCount=" + starCount + - ", watcherCount=" + watcherCount + + "publicVersions=" + this.publicVersions + + ", flagCount=" + this.flagCount + + ", noteCount=" + this.noteCount + + ", starCount=" + this.starCount + + ", watcherCount=" + this.watcherCount + '}'; } } @@ -167,7 +174,7 @@ public class HangarProject extends Project implements Joinable private final Map platformDependenciesFormatted; private final Map downloads; - public PinnedVersion(long versionId, Type type, String name, @Nested("pc") ProjectChannel channel) { + public PinnedVersion(final long versionId, final Type type, final String name, @Nested("pc") final ProjectChannel channel) { this.versionId = versionId; this.type = type; this.name = name; @@ -177,38 +184,38 @@ public class HangarProject extends Project implements Joinable } public long getVersionId() { - return versionId; + return this.versionId; } public Type getType() { - return type; + return this.type; } public String getName() { - return name; + return this.name; } public Map getPlatformDependenciesFormatted() { - return platformDependenciesFormatted; + return this.platformDependenciesFormatted; } public ProjectChannel getChannel() { - return channel; + return this.channel; } public Map getDownloads() { - return downloads; + return this.downloads; } @Override public String toString() { return "PinnedVersion{" + - "versionId=" + versionId + - ", type=" + type + - ", name='" + name + '\'' + - ", channel=" + channel + - ", platformDependenciesFormatted=" + platformDependenciesFormatted + - ", downloads=" + downloads + + "versionId=" + this.versionId + + ", type=" + this.type + + ", name='" + this.name + '\'' + + ", channel=" + this.channel + + ", platformDependenciesFormatted=" + this.platformDependenciesFormatted + + ", downloads=" + this.downloads + '}'; } diff --git a/backend/src/main/java/io/papermc/hangar/service/internal/file/LocalStorageFileService.java b/backend/src/main/java/io/papermc/hangar/service/internal/file/LocalStorageFileService.java index cef070b34..be605adb9 100644 --- a/backend/src/main/java/io/papermc/hangar/service/internal/file/LocalStorageFileService.java +++ b/backend/src/main/java/io/papermc/hangar/service/internal/file/LocalStorageFileService.java @@ -21,49 +21,49 @@ public class LocalStorageFileService implements FileService { private final StorageConfig config; private final HangarConfig hangarConfig; - public LocalStorageFileService(StorageConfig config, HangarConfig hangarConfig) { + public LocalStorageFileService(final StorageConfig config, final HangarConfig hangarConfig) { this.config = config; this.hangarConfig = hangarConfig; } @Override - public Resource getResource(String path) { + public Resource getResource(final String path) { return new FileSystemResource(path); } @Override - public boolean exists(String path) { + public boolean exists(final String path) { return Files.exists(Path.of(path)); } @Override - public void deleteDirectory(String dir) { + public void deleteDirectory(final String dir) { FileUtils.deleteDirectory(Path.of(dir)); } @Override - public boolean delete(String path) { + public boolean delete(final String path) { return FileUtils.delete(Path.of(path)); } @Override - public byte[] bytes(String path) throws IOException { + public byte[] bytes(final String path) throws IOException { return Files.readAllBytes(Path.of(path)); } @Override - public void write(InputStream inputStream, String path) throws IOException { - Path p = Path.of(path); + public void write(final InputStream inputStream, final String path) throws IOException { + final Path p = Path.of(path); if (Files.notExists(p)) { Files.createDirectories(p.getParent()); } - Files.copy(inputStream, p); + Files.copy(inputStream, p, StandardCopyOption.REPLACE_EXISTING); } @Override - public void move(String oldPathString, String newPathString) throws IOException { - Path oldPath = Path.of(oldPathString); - Path newPath = Path.of(newPathString); + public void move(final String oldPathString, final String newPathString) throws IOException { + final Path oldPath = Path.of(oldPathString); + final Path newPath = Path.of(newPathString); if (Files.notExists(newPath)) { Files.createDirectories(newPath.getParent()); } @@ -75,9 +75,9 @@ public class LocalStorageFileService implements FileService { } @Override - public void link(String existingPathString, String newPathString) throws IOException { - Path existingPath = Path.of(existingPathString); - Path newPath = Path.of(newPathString); + public void link(final String existingPathString, final String newPathString) throws IOException { + final Path existingPath = Path.of(existingPathString); + final Path newPath = Path.of(newPathString); if (Files.notExists(newPath)) { Files.createDirectories(newPath.getParent()); } @@ -85,17 +85,17 @@ public class LocalStorageFileService implements FileService { } @Override - public String resolve(String path, String fileName) { + public String resolve(final String path, final String fileName) { return Path.of(path).resolve(fileName).toString(); } @Override public String getRoot() { - return config.workDir(); + return this.config.workDir(); } @Override - public String getDownloadUrl(String user, String project, String version, Platform platform, String fileName) { - return hangarConfig.getBaseUrl() + "/api/v1/projects/" + user + "/" + project + "/versions/" + version + "/" + platform.name() + "/download"; + public String getDownloadUrl(final String user, final String project, final String version, final Platform platform, final String fileName) { + return this.hangarConfig.getBaseUrl() + "/api/v1/projects/" + user + "/" + project + "/versions/" + version + "/" + platform.name() + "/download"; } } diff --git a/backend/src/main/java/io/papermc/hangar/service/internal/projects/ProjectService.java b/backend/src/main/java/io/papermc/hangar/service/internal/projects/ProjectService.java index fa1306a01..3dd49d332 100644 --- a/backend/src/main/java/io/papermc/hangar/service/internal/projects/ProjectService.java +++ b/backend/src/main/java/io/papermc/hangar/service/internal/projects/ProjectService.java @@ -154,7 +154,7 @@ public class ProjectService extends HangarComponent { } } - return new HangarProject(project.getRight(), project.getLeft(), projectOwner, members, lastVisibilityChangeComment, lastVisibilityChangeUserName, info, pages.values(), pinnedVersions, mainChannelVersions); + return new HangarProject(project.getRight(), project.getLeft(), projectOwner, members, lastVisibilityChangeComment, lastVisibilityChangeUserName, info, pages.values(), pinnedVersions, mainChannelVersions, this.fileService.exists(this.projectFiles.getIconPath(author, slug))); } public @Nullable HangarVersion getLastVersion(String author, String slug, Platform platform, @Nullable String channel) { diff --git a/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ImageService.java b/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ImageService.java index 81f5d3df2..1172b8e9a 100644 --- a/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ImageService.java +++ b/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ImageService.java @@ -4,16 +4,13 @@ import io.papermc.hangar.HangarComponent; import io.papermc.hangar.exceptions.HangarApiException; import io.papermc.hangar.exceptions.InternalHangarException; import io.papermc.hangar.service.internal.file.FileService; +import java.io.IOException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.CacheControl; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - @Service public class ImageService extends HangarComponent { @@ -21,27 +18,25 @@ public class ImageService extends HangarComponent { private final FileService fileService; @Autowired - public ImageService(ProjectFiles projectFiles, FileService fileService) { + public ImageService(final ProjectFiles projectFiles, final FileService fileService) { this.projectFiles = projectFiles; this.fileService = fileService; } - public ResponseEntity getProjectIcon(String author, String slug) { - String iconPath = projectFiles.getIconPath(author, slug); - if (iconPath == null || !fileService.exists(iconPath)) { + public ResponseEntity getProjectIcon(final String author, final String slug) { + final String iconPath = this.projectFiles.getIconPath(author, slug); + if (iconPath == null || !this.fileService.exists(iconPath)) { throw new InternalHangarException("Default to avatar url"); } try { - HttpHeaders headers = new HttpHeaders(); - headers.setCacheControl(CacheControl.maxAge(3600, TimeUnit.SECONDS).getHeaderValue()); - return ResponseEntity.ok().headers(headers).body(fileService.bytes(iconPath)); - } catch (IOException e) { + return ResponseEntity.ok().cacheControl(CacheControl.noCache()).body(this.fileService.bytes(iconPath)); + } catch (final IOException e) { e.printStackTrace(); throw new HangarApiException(HttpStatus.INTERNAL_SERVER_ERROR, "Unable to fetch project icon"); } } - public String getUserIcon(String author) { - return String.format(config.security.api().avatarUrl(), author); + public String getUserIcon(final String author) { + return String.format(this.config.security.api().avatarUrl(), author); } } diff --git a/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ProjectFiles.java b/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ProjectFiles.java index 7ae00fcdb..e40cabd9a 100644 --- a/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ProjectFiles.java +++ b/backend/src/main/java/io/papermc/hangar/service/internal/uploads/ProjectFiles.java @@ -4,15 +4,14 @@ import io.papermc.hangar.config.hangar.StorageConfig; import io.papermc.hangar.model.common.Platform; import io.papermc.hangar.service.internal.file.FileService; import io.papermc.hangar.util.FileUtils; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - @Component public class ProjectFiles { @@ -23,77 +22,77 @@ public class ProjectFiles { private final FileService fileService; @Autowired - public ProjectFiles(StorageConfig storageConfig, FileService fileService) { + public ProjectFiles(final StorageConfig storageConfig, final FileService fileService) { this.fileService = fileService; - Path uploadsDir = Path.of(storageConfig.workDir()); - pluginsDir = fileService.resolve(fileService.getRoot(), "plugins"); - tmpDir = uploadsDir.resolve("tmp"); - if (Files.exists(tmpDir)) { - FileUtils.deleteDirectory(tmpDir); + final Path uploadsDir = Path.of(storageConfig.workDir()); + this.pluginsDir = fileService.resolve(fileService.getRoot(), "plugins"); + this.tmpDir = uploadsDir.resolve("tmp"); + if (Files.exists(this.tmpDir)) { + FileUtils.deleteDirectory(this.tmpDir); } logger.info("Cleaned up tmp files and inited work dir {} ", uploadsDir); } - public String getProjectDir(String owner, String name) { - return fileService.resolve(getUserDir(owner), name); + public String getProjectDir(final String owner, final String slug) { + return this.fileService.resolve(this.getUserDir(owner), slug); } - public String getVersionDir(String owner, String name, String version) { - return fileService.resolve(fileService.resolve(getProjectDir(owner, name), "versions"), version); + public String getVersionDir(final String owner, final String slug, final String version) { + return this.fileService.resolve(this.fileService.resolve(this.getProjectDir(owner, slug), "versions"), version); } - public String getVersionDir(String owner, String name, String version, Platform platform) { - return fileService.resolve(getVersionDir(owner, name, version), platform.name()); + public String getVersionDir(final String owner, final String slug, final String version, final Platform platform) { + return this.fileService.resolve(this.getVersionDir(owner, slug, version), platform.name()); } - public String getVersionDir(String owner, String name, String version, Platform platform, String fileName) { - return fileService.resolve(getVersionDir(owner, name, version, platform), fileName); + public String getVersionDir(final String owner, final String name, final String version, final Platform platform, final String fileName) { + return this.fileService.resolve(this.getVersionDir(owner, name, version, platform), fileName); } - public String getUserDir(String user) { - return fileService.resolve(pluginsDir, user); + public String getUserDir(final String user) { + return this.fileService.resolve(this.pluginsDir, user); } - public void transferProject(String owner, String newOwner, String slug) { - final String oldProjectDir = getProjectDir(owner, slug); - final String newProjectDir = getProjectDir(newOwner, slug); + public void transferProject(final String owner, final String newOwner, final String slug) { + final String oldProjectDir = this.getProjectDir(owner, slug); + final String newProjectDir = this.getProjectDir(newOwner, slug); try { - fileService.move(oldProjectDir, newProjectDir); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void renameProject(String owner, String slug, String newSlug) { - final String oldProjectDir = getProjectDir(owner, slug); - final String newProjectDir = getProjectDir(owner, newSlug); - try { - fileService.move(oldProjectDir, newProjectDir); + this.fileService.move(oldProjectDir, newProjectDir); } catch (final IOException e) { e.printStackTrace(); } } - public void renameVersion(String owner, String slug, String version, String newVersionName) { - final String oldVersionDir = getVersionDir(owner, slug, version); - final String newVersionDir = getVersionDir(owner, slug, newVersionName); + public void renameProject(final String owner, final String slug, final String newSlug) { + final String oldProjectDir = this.getProjectDir(owner, slug); + final String newProjectDir = this.getProjectDir(owner, newSlug); try { - fileService.move(oldVersionDir, newVersionDir); + this.fileService.move(oldProjectDir, newProjectDir); } catch (final IOException e) { e.printStackTrace(); } } - public String getIconsDir(String owner, String name) { - return fileService.resolve(getProjectDir(owner, name), "icons"); + public void renameVersion(final String owner, final String slug, final String version, final String newVersionName) { + final String oldVersionDir = this.getVersionDir(owner, slug, version); + final String newVersionDir = this.getVersionDir(owner, slug, newVersionName); + try { + this.fileService.move(oldVersionDir, newVersionDir); + } catch (final IOException e) { + e.printStackTrace(); + } } - public String getIconPath(String owner, String name) { - return fileService.resolve(getIconsDir(owner, name), "icon.png"); + public String getIconsDir(final String owner, final String slug) { + return this.fileService.resolve(this.getProjectDir(owner, slug), "icons"); } - public Path getTempDir(String owner) { - return tmpDir.resolve(owner); + public String getIconPath(final String owner, final String slug) { + return this.fileService.resolve(this.getIconsDir(owner, slug), "icon.png"); + } + + public Path getTempDir(final String owner) { + return this.tmpDir.resolve(owner); } } diff --git a/frontend/src/pages/[user]/[project]/settings.vue b/frontend/src/pages/[user]/[project]/settings.vue index 8b86eb54c..7c9d7aeab 100644 --- a/frontend/src/pages/[user]/[project]/settings.vue +++ b/frontend/src/pages/[user]/[project]/settings.vue @@ -9,7 +9,7 @@ import { useVuelidate } from "@vuelidate/core"; import { Cropper, type CropperResult } from "vue-advanced-cropper"; import { PaginatedResult, User } from "hangar-api"; import { useSeo } from "~/composables/useSeo"; -import { projectIconUrl } from "~/composables/useUrlHelper"; +import { avatarUrl, projectIconUrl } from "~/composables/useUrlHelper"; import Card from "~/lib/components/design/Card.vue"; import MemberList from "~/components/projects/MemberList.vue"; import { hasPerms } from "~/composables/usePerm"; @@ -65,11 +65,13 @@ if (!form.settings.license.type) { form.settings.license.type = "Unspecified"; } +const hasCustomIcon = ref(props.project.customIcon); const projectIcon = ref(null); const cropperInput = ref(); const cropperResult = ref(); +const imgSrc = ref(projectIconUrl(props.project.namespace.owner, props.project.namespace.slug)); let reader: FileReader | null = null; -onMounted(async () => { +onMounted(() => { reader = new FileReader(); reader.addEventListener( "load", @@ -78,7 +80,6 @@ onMounted(async () => { }, false ); - await loadIconIntoCropper(); }); watch(projectIcon, (newValue) => { @@ -93,16 +94,6 @@ function changeImage({ canvas }: CropperResult) { }); } -async function loadIconIntoCropper() { - const response = await fetch(projectIconUrl(props.project.namespace.owner, props.project.namespace.slug, false), { - headers: { - "Cache-Control": "no-cache", - }, - }); - const data = await response.blob(); - reader?.readAsDataURL(data); -} - const newName = ref(""); const newNameField = ref | null>(null); const loading = reactive({ @@ -210,8 +201,11 @@ async function uploadIcon() { loading.uploadIcon = true; try { const response = await useInternalApi(`projects/project/${route.params.user}/${route.params.project}/saveIcon`, "post", data); + imgSrc.value = URL.createObjectURL(cropperResult.value); // set temporary source so it changes right away + projectIcon.value = null; + cropperInput.value = null; cropperResult.value = null; - await loadIconIntoCropper(); + hasCustomIcon.value = true; if (response) { useNotificationStore().success(i18n.t("project.settings.success.changedIconWarn", [response])); } else { @@ -232,8 +226,11 @@ async function resetIcon() { } else { useNotificationStore().success(i18n.t("project.settings.success.resetIcon")); } + imgSrc.value = avatarUrl(props.project.owner.name); // set temporary source so it changes right away projectIcon.value = null; - await loadIconIntoCropper(); + cropperInput.value = null; + cropperResult.value = null; + hasCustomIcon.value = false; } catch (e: any) { handleRequestError(e); } @@ -305,11 +302,11 @@ useHead( {{ i18n.t("project.settings.iconUpload") }} - -
+
- Project Icon + Project Icon
diff --git a/frontend/src/types/generated/icons.d.ts b/frontend/src/types/generated/icons.d.ts index 454efa647..fbb7d711f 100644 --- a/frontend/src/types/generated/icons.d.ts +++ b/frontend/src/types/generated/icons.d.ts @@ -14,6 +14,7 @@ declare module "@vue/runtime-core" { IconMdiAlertOutline: typeof import("~icons/mdi/alert-outline")["default"]; IconMdiBell: typeof import("~icons/mdi/bell")["default"]; IconMdiBellOutline: typeof import("~icons/mdi/bell-outline")["default"]; + IconMdiCached: typeof import("~icons/mdi/cached")["default"]; IconMdiCalendar: typeof import("~icons/mdi/calendar")["default"]; IconMdiCancel: typeof import("~icons/mdi/cancel")["default"]; IconMdiCashMultiple: typeof import("~icons/mdi/cash-multiple")["default"]; diff --git a/frontend/src/types/internal/projects.d.ts b/frontend/src/types/internal/projects.d.ts index 81d80d71f..087198d98 100644 --- a/frontend/src/types/internal/projects.d.ts +++ b/frontend/src/types/internal/projects.d.ts @@ -44,6 +44,7 @@ declare module "hangar-internal" { pages: HangarProjectPage[]; pinnedVersions: PinnedVersion[]; mainChannelVersions: Record; + customIcon: boolean; } interface ProjectPage extends Table {