forked from didirus/AstralRinth
Settings refactor and redesign (#1669)
* new settings work * Polishing work on settings refactor * Run intl:extract * List view -> Rows view * Remove current preferred system theme indicator to make the themes fit on one line * Remove extra margin on top of navstack
This commit is contained in:
1
assets/images/utils/monitor-smartphone.svg
Normal file
1
assets/images/utils/monitor-smartphone.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-monitor-smartphone"><path d="M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8"/><path d="M10 19v-3.96 3.15"/><path d="M7 19h5"/><rect width="6" height="10" x="16" y="12" rx="2"/></svg>
|
||||
|
After Width: | Height: | Size: 393 B |
@@ -25,6 +25,7 @@
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: var(--spacing-card-lg);
|
||||
gap: var(--spacing-card-md);
|
||||
outline: 1px solid transparent;
|
||||
|
||||
.label {
|
||||
color: var(--color-heading);
|
||||
@@ -65,6 +66,7 @@
|
||||
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
|
||||
box-shadow: var(--shadow-card);
|
||||
|
||||
@@ -687,7 +689,7 @@ tr.button-transparent {
|
||||
}
|
||||
|
||||
color: var(--color-text) !important;
|
||||
outline: 2px solid transparent;
|
||||
outline: none !important;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
@@ -706,6 +708,7 @@ tr.button-transparent {
|
||||
cursor: pointer;
|
||||
padding-left: 7px;
|
||||
padding-top: 10px;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
|
||||
@@ -753,8 +756,13 @@ tr.button-transparent {
|
||||
border-bottom-left-radius: var(--size-rounded-sm);
|
||||
border-bottom-right-radius: var(--size-rounded-sm);
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
|
||||
outline: 2px solid transparent;
|
||||
|
||||
.multiselect__element {
|
||||
.multiselect__option {
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight {
|
||||
background: var(--color-button-bg-active);
|
||||
color: var(--color-text-dark);
|
||||
@@ -894,31 +902,6 @@ tr.button-transparent {
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
min-height: var(--font-size-2xl);
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
outline: 2px solid transparent;
|
||||
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
box-shadow: var(--shadow-card);
|
||||
|
||||
.card__overlay {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
grid-gap: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
background-color: var(--color-divider);
|
||||
border: none;
|
||||
@@ -1082,6 +1065,7 @@ button {
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
.text-input-wrapper__before {
|
||||
display: flex;
|
||||
@@ -1102,9 +1086,10 @@ button {
|
||||
textarea {
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: unset;
|
||||
box-shadow: unset !important;
|
||||
padding-left: 0;
|
||||
flex-grow: 1;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -288,6 +288,29 @@ html {
|
||||
--color-ad: #0d1828;
|
||||
}
|
||||
|
||||
.retro-mode {
|
||||
--color-bg: #191917;
|
||||
--color-raised-bg: #1d1e1b;
|
||||
--color-button-bg: #3a3b38;
|
||||
--color-base: #c3c4b3;
|
||||
--color-secondary: #777a74;
|
||||
--color-contrast: #e6e2d1;
|
||||
|
||||
--color-brand: #4d9227;
|
||||
--color-brand-highlight: #25421e;
|
||||
--color-ad: var(--color-brand-highlight);
|
||||
--color-ad-raised: var(--color-brand);
|
||||
--color-ad-contrast: black;
|
||||
--color-ad-highlight: var(--color-brand);
|
||||
|
||||
--color-red: rgb(232, 32, 13);
|
||||
--color-orange: rgb(232, 141, 13);
|
||||
--color-green: rgb(60, 219, 54);
|
||||
--color-blue: rgb(9, 159, 239);
|
||||
--color-purple: rgb(139, 129, 230);
|
||||
--color-gray: #718096;
|
||||
}
|
||||
|
||||
body {
|
||||
// Defaults
|
||||
background-color: var(--color-bg);
|
||||
@@ -457,3 +480,21 @@ a:focus-visible,
|
||||
outline: 0.25rem solid #ea80ff;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
// OMORPHIA FIXES
|
||||
.card {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: 2px solid transparent !important;
|
||||
}
|
||||
|
||||
.button-animation {
|
||||
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out,
|
||||
outline-width 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.button-transparent {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="checkbox-outer button-within"
|
||||
:class="{ disabled }"
|
||||
:class="{ disabled, checked: modelValue }"
|
||||
role="presentation"
|
||||
@click="toggle"
|
||||
>
|
||||
@@ -82,6 +82,12 @@ export default {
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 4px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
|
||||
@@ -24,6 +24,7 @@ const ariaLabelByType = computed(() => `Banner with ${props.messageType} message
|
||||
border-radius: var(--size-rounded-card);
|
||||
overflow: hidden;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
pointer-events: auto;
|
||||
outline: 3px solid transparent;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -134,6 +134,12 @@ export default {
|
||||
&.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
|
||||
&:not(:focus-visible) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 6px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -157,6 +163,8 @@ export default {
|
||||
transition: all ease-in-out 0.2s;
|
||||
border-radius: var(--size-rounded-max);
|
||||
background-color: var(--color-brand);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition: none !important;
|
||||
|
||||
@@ -19,6 +19,10 @@ ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
@@ -70,6 +70,7 @@ export default {
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
:where(.nav-link) {
|
||||
--text-color: var(--color-text);
|
||||
@@ -94,6 +95,9 @@ export default {
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
outline: 2px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
.nav-content {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-button-bg);
|
||||
|
||||
@@ -133,6 +133,7 @@ a {
|
||||
background: var(--color-brand);
|
||||
color: var(--color-brand-inverted);
|
||||
cursor: default;
|
||||
outline: 2px solid transparent;
|
||||
}
|
||||
|
||||
&.paginate.disabled {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const useCosmetics = () =>
|
||||
developerMode: false,
|
||||
notUsingBlockers: false,
|
||||
hideModrinthAppPromos: false,
|
||||
preferredDarkTheme: 'dark',
|
||||
searchDisplayMode: {
|
||||
mod: 'list',
|
||||
plugin: 'list',
|
||||
|
||||
@@ -24,6 +24,7 @@ export const useTheme = () =>
|
||||
|
||||
export const updateTheme = (value, updatePreference = false) => {
|
||||
const theme = useTheme()
|
||||
const cosmetics = useCosmetics()
|
||||
|
||||
const themeCookie = useCookie('color-mode', {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
@@ -40,7 +41,7 @@ export const updateTheme = (value, updatePreference = false) => {
|
||||
if (colorSchemeQueryList.matches) {
|
||||
theme.value.value = 'light'
|
||||
} else {
|
||||
theme.value.value = 'dark'
|
||||
theme.value.value = cosmetics.value.preferredDarkTheme
|
||||
}
|
||||
} else {
|
||||
theme.value.value = value
|
||||
@@ -53,3 +54,5 @@ export const updateTheme = (value, updatePreference = false) => {
|
||||
|
||||
themeCookie.value = theme.value
|
||||
}
|
||||
|
||||
export const DARK_THEMES = ['dark', 'oled', 'retro']
|
||||
|
||||
@@ -433,6 +433,7 @@ import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
|
||||
import { commonMessages } from '~/utils/common-messages.ts'
|
||||
import { DARK_THEMES } from '~/composables/theme.js'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -724,7 +725,10 @@ function toggleBrowseMenu() {
|
||||
}
|
||||
}
|
||||
function changeTheme() {
|
||||
updateTheme(app.$colorMode.value === 'dark' ? 'light' : 'dark', true)
|
||||
updateTheme(
|
||||
DARK_THEMES.includes(app.$colorMode.value) ? 'light' : cosmetics.value.preferredDarkTheme,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
function hideStagingBanner() {
|
||||
@@ -781,6 +785,15 @@ function hideStagingBanner() {
|
||||
a {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
&:not(:focus-visible) {
|
||||
outline: none;
|
||||
|
||||
&.router-link-exact-active {
|
||||
outline: 2px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.small-logo {
|
||||
@@ -909,6 +922,7 @@ function hideStagingBanner() {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
|
||||
.user-icon {
|
||||
height: 2rem;
|
||||
@@ -959,6 +973,7 @@ function hideStagingBanner() {
|
||||
display: flex;
|
||||
padding: 0.5rem 0.75rem;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5rem;
|
||||
@@ -969,6 +984,7 @@ function hideStagingBanner() {
|
||||
&.router-link-exact-active {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-button-bg);
|
||||
outline: 2px solid transparent;
|
||||
|
||||
&.primary-color {
|
||||
color: var(--color-button-text-active);
|
||||
|
||||
@@ -191,6 +191,9 @@
|
||||
"button.sign-out": {
|
||||
"message": "Sign out"
|
||||
},
|
||||
"button.upload-image": {
|
||||
"message": "Upload image"
|
||||
},
|
||||
"collection.button.delete-icon": {
|
||||
"message": "Delete icon"
|
||||
},
|
||||
@@ -285,7 +288,10 @@
|
||||
"message": "Grid view"
|
||||
},
|
||||
"input.view.list": {
|
||||
"message": "List view"
|
||||
"message": "Rows view"
|
||||
},
|
||||
"label.changes-saved": {
|
||||
"message": "Changes saved"
|
||||
},
|
||||
"label.collections": {
|
||||
"message": "Collections"
|
||||
@@ -779,6 +785,114 @@
|
||||
"scopes.versionWrite.label": {
|
||||
"message": "Write versions"
|
||||
},
|
||||
"settings.account.title": {
|
||||
"message": "Account and security"
|
||||
},
|
||||
"settings.appearance.title": {
|
||||
"message": "Appearance"
|
||||
},
|
||||
"settings.applications.title": {
|
||||
"message": "Your applications"
|
||||
},
|
||||
"settings.authorized-apps.title": {
|
||||
"message": "Authorized apps"
|
||||
},
|
||||
"settings.display.banner.developer-mode.button": {
|
||||
"message": "Deactivate developer mode"
|
||||
},
|
||||
"settings.display.banner.developer-mode.description": {
|
||||
"message": "<strong>Developer mode</strong> is active. This will allow you to view the internal IDs of various things throughout Modrinth that may be helpful if you're a developer using the Modrinth API. Click on the Modrinth logo at the bottom of the page 5 times to toggle developer mode."
|
||||
},
|
||||
"settings.display.flags.description": {
|
||||
"message": "Enable or disable certain features on this device."
|
||||
},
|
||||
"settings.display.flags.title": {
|
||||
"message": "Feature flags"
|
||||
},
|
||||
"settings.display.project-list-layouts.datapack": {
|
||||
"message": "Data Packs page"
|
||||
},
|
||||
"settings.display.project-list-layouts.description": {
|
||||
"message": "Select your preferred layout for each page that displays project lists on this device."
|
||||
},
|
||||
"settings.display.project-list-layouts.mod": {
|
||||
"message": "Mods page"
|
||||
},
|
||||
"settings.display.project-list-layouts.modpack": {
|
||||
"message": "Modpacks page"
|
||||
},
|
||||
"settings.display.project-list-layouts.plugin": {
|
||||
"message": "Plugins page"
|
||||
},
|
||||
"settings.display.project-list-layouts.resourcepack": {
|
||||
"message": "Resource Packs page"
|
||||
},
|
||||
"settings.display.project-list-layouts.shader": {
|
||||
"message": "Shaders page"
|
||||
},
|
||||
"settings.display.project-list-layouts.title": {
|
||||
"message": "Project list layouts"
|
||||
},
|
||||
"settings.display.project-list-layouts.user": {
|
||||
"message": "User profile pages"
|
||||
},
|
||||
"settings.display.sidebar.advanced-rendering.description": {
|
||||
"message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering."
|
||||
},
|
||||
"settings.display.sidebar.advanced-rendering.title": {
|
||||
"message": "Advanced rendering"
|
||||
},
|
||||
"settings.display.sidebar.external-links-new-tab.description": {
|
||||
"message": "Make links which go outside of Modrinth open in a new tab. No matter this setting, links on the same domain and in Markdown descriptions will open in the same tab, and links on ads and edit pages will open in a new tab."
|
||||
},
|
||||
"settings.display.sidebar.external-links-new-tab.title": {
|
||||
"message": "Open external links in new tab"
|
||||
},
|
||||
"settings.display.sidebar.hide-app-promos.description": {
|
||||
"message": "Hides the \"Get Modrinth App\" buttons from primary navigation. The Modrinth App page can still be found on the landing page or in the footer."
|
||||
},
|
||||
"settings.display.sidebar.hide-app-promos.title": {
|
||||
"message": "Hide Modrinth App promotions"
|
||||
},
|
||||
"settings.display.sidebar.right-aligned-project-sidebar.description": {
|
||||
"message": "Aligns the project details sidebar to the right of the page's content."
|
||||
},
|
||||
"settings.display.sidebar.right-aligned-project-sidebar.title": {
|
||||
"message": "Right-aligned project sidebar"
|
||||
},
|
||||
"settings.display.sidebar.right-aligned-search-sidebar.description": {
|
||||
"message": "Aligns the search filters sidebar to the right of the search results."
|
||||
},
|
||||
"settings.display.sidebar.right-aligned-search-sidebar.title": {
|
||||
"message": "Right-aligned search sidebar"
|
||||
},
|
||||
"settings.display.theme.dark": {
|
||||
"message": "Dark"
|
||||
},
|
||||
"settings.display.theme.description": {
|
||||
"message": "Select your preferred color theme for Modrinth on this device."
|
||||
},
|
||||
"settings.display.theme.light": {
|
||||
"message": "Light"
|
||||
},
|
||||
"settings.display.theme.oled": {
|
||||
"message": "OLED"
|
||||
},
|
||||
"settings.display.theme.preferred-dark-theme": {
|
||||
"message": "Preferred dark theme"
|
||||
},
|
||||
"settings.display.theme.preferred-light-theme": {
|
||||
"message": "Preferred light theme"
|
||||
},
|
||||
"settings.display.theme.retro": {
|
||||
"message": "Retro"
|
||||
},
|
||||
"settings.display.theme.system": {
|
||||
"message": "Sync with system"
|
||||
},
|
||||
"settings.display.theme.title": {
|
||||
"message": "Color theme"
|
||||
},
|
||||
"settings.language.categories.auto": {
|
||||
"message": "Automatic"
|
||||
},
|
||||
@@ -858,10 +972,7 @@
|
||||
"message": "Edit personal access token"
|
||||
},
|
||||
"settings.pats.title": {
|
||||
"message": "PATs"
|
||||
},
|
||||
"settings.pats.title.long": {
|
||||
"message": "Personal Access Tokens"
|
||||
"message": "Personal access tokens"
|
||||
},
|
||||
"settings.pats.token.action.edit": {
|
||||
"message": "Edit token"
|
||||
@@ -881,6 +992,36 @@
|
||||
"settings.pats.token.never-used": {
|
||||
"message": "Never used"
|
||||
},
|
||||
"settings.profile.bio.description": {
|
||||
"message": "A short description to tell everyone a little bit about you."
|
||||
},
|
||||
"settings.profile.bio.title": {
|
||||
"message": "Bio"
|
||||
},
|
||||
"settings.profile.description": {
|
||||
"message": "Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>."
|
||||
},
|
||||
"settings.profile.profile-info": {
|
||||
"message": "Profile information"
|
||||
},
|
||||
"settings.profile.profile-picture.reset": {
|
||||
"message": "Reset"
|
||||
},
|
||||
"settings.profile.profile-picture.title": {
|
||||
"message": "Profile picture"
|
||||
},
|
||||
"settings.profile.title": {
|
||||
"message": "Public profile"
|
||||
},
|
||||
"settings.profile.username.description": {
|
||||
"message": "A unique case-insensitive name to identify your profile."
|
||||
},
|
||||
"settings.profile.username.title": {
|
||||
"message": "Username"
|
||||
},
|
||||
"settings.profile.visit-profile": {
|
||||
"message": "Visit your profile"
|
||||
},
|
||||
"settings.sessions.action.revoke-session": {
|
||||
"message": "Revoke session"
|
||||
},
|
||||
|
||||
@@ -502,6 +502,7 @@ export default defineNuxtComponent({
|
||||
border-radius: var(--size-rounded-sm);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-card-md);
|
||||
outline: 1px solid transparent;
|
||||
|
||||
.grid-table__row {
|
||||
display: contents;
|
||||
|
||||
@@ -1,65 +1,116 @@
|
||||
<template>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<h1>Settings</h1>
|
||||
<NavStack>
|
||||
<NavStackItem link="/settings" label="Appearance">
|
||||
<PaintbrushIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem v-if="isStaging" link="/settings/language" label="Language">
|
||||
<LanguagesIcon />
|
||||
</NavStackItem>
|
||||
<template v-if="auth.user">
|
||||
<h3>User settings</h3>
|
||||
<NavStackItem link="/settings/account" label="Account">
|
||||
<UserIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/authorizations" label="Authorizations">
|
||||
<UsersIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/sessions" :label="formatMessage(messages.sessionsTitle)">
|
||||
<ShieldIcon />
|
||||
</NavStackItem>
|
||||
</template>
|
||||
<template v-if="auth.user">
|
||||
<h3>Developer Settings</h3>
|
||||
<NavStackItem link="/settings/pats" :label="formatMessage(messages.patsTitle)">
|
||||
<KeyIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/applications" label="Applications">
|
||||
<ServerIcon />
|
||||
</NavStackItem>
|
||||
</template>
|
||||
</NavStack>
|
||||
</aside>
|
||||
<div>
|
||||
<div class="normal-page no-sidebar">
|
||||
<h1>{{ formatMessage(commonMessages.settingsLabel) }}</h1>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtPage :route="route" />
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<aside class="universal-card">
|
||||
<NavStack>
|
||||
<h3>Display</h3>
|
||||
<NavStackItem link="/settings" :label="formatMessage(messages.appearanceTitle)">
|
||||
<PaintBrushIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
v-if="isStaging"
|
||||
link="/settings/language"
|
||||
:label="formatMessage(messages.languageTitle)"
|
||||
>
|
||||
<LanguagesIcon />
|
||||
</NavStackItem>
|
||||
<template v-if="auth.user">
|
||||
<h3>Account</h3>
|
||||
<NavStackItem link="/settings/profile" :label="formatMessage(messages.profileTitle)">
|
||||
<UserIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem link="/settings/account" :label="formatMessage(messages.accountTitle)">
|
||||
<ShieldIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
link="/settings/authorizations"
|
||||
:label="formatMessage(messages.authorizedAppsTitle)"
|
||||
>
|
||||
<GridIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
link="/settings/sessions"
|
||||
:label="formatMessage(messages.sessionsTitle)"
|
||||
>
|
||||
<MonitorSmartphoneIcon />
|
||||
</NavStackItem>
|
||||
</template>
|
||||
<template v-if="auth.user">
|
||||
<h3>Developer</h3>
|
||||
<NavStackItem link="/settings/pats" :label="formatMessage(messages.patsTitle)">
|
||||
<KeyIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
link="/settings/applications"
|
||||
:label="formatMessage(messages.applicationsTitle)"
|
||||
>
|
||||
<ServerIcon />
|
||||
</NavStackItem>
|
||||
</template>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtPage :route="route" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { UsersIcon, ServerIcon } from 'omorphia'
|
||||
import {
|
||||
UsersIcon,
|
||||
ServerIcon,
|
||||
GridIcon,
|
||||
PaintBrushIcon,
|
||||
UserIcon,
|
||||
ShieldIcon,
|
||||
KeyIcon,
|
||||
LanguagesIcon,
|
||||
} from 'omorphia'
|
||||
import NavStack from '~/components/ui/NavStack.vue'
|
||||
import NavStackItem from '~/components/ui/NavStackItem.vue'
|
||||
import MonitorSmartphoneIcon from '~/assets/images/utils/monitor-smartphone.svg'
|
||||
|
||||
import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import ShieldIcon from '~/assets/images/utils/shield.svg'
|
||||
import KeyIcon from '~/assets/images/utils/key.svg'
|
||||
import LanguagesIcon from '~/assets/images/utils/languages.svg'
|
||||
import { commonMessages } from '~/utils/common-messages.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
appearanceTitle: {
|
||||
id: 'settings.appearance.title',
|
||||
defaultMessage: 'Appearance',
|
||||
},
|
||||
languageTitle: {
|
||||
id: 'settings.language.title',
|
||||
defaultMessage: 'Language',
|
||||
},
|
||||
profileTitle: {
|
||||
id: 'settings.profile.title',
|
||||
defaultMessage: 'Public profile',
|
||||
},
|
||||
accountTitle: {
|
||||
id: 'settings.account.title',
|
||||
defaultMessage: 'Account and security',
|
||||
},
|
||||
authorizedAppsTitle: {
|
||||
id: 'settings.authorized-apps.title',
|
||||
defaultMessage: 'Authorized apps',
|
||||
},
|
||||
sessionsTitle: {
|
||||
id: 'settings.sessions.title',
|
||||
defaultMessage: 'Sessions',
|
||||
},
|
||||
patsTitle: {
|
||||
id: 'settings.pats.title',
|
||||
defaultMessage: 'PATs',
|
||||
defaultMessage: 'Personal access tokens',
|
||||
},
|
||||
applicationsTitle: {
|
||||
id: 'settings.applications.title',
|
||||
defaultMessage: 'Your applications',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -290,14 +290,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<section class="universal-card">
|
||||
<h2>User profile</h2>
|
||||
<p>Visit your user profile to edit your profile information.</p>
|
||||
<NuxtLink class="iconified-button" :to="`/user/${auth.user.username}`">
|
||||
<UserIcon /> Visit your profile
|
||||
</NuxtLink>
|
||||
</section>
|
||||
|
||||
<section class="universal-card">
|
||||
<h2>Account security</h2>
|
||||
|
||||
|
||||
@@ -1,96 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<MessageBanner v-if="cosmetics.developerMode" message-type="warning" class="developer-message">
|
||||
<CodeIcon />
|
||||
<IntlFormatted :message-id="developerModeBanner.description">
|
||||
<template #strong="{ children }">
|
||||
<strong>
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</strong>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
<Button :action="() => disableDeveloperMode()">
|
||||
{{ formatMessage(developerModeBanner.deactivate) }}
|
||||
</Button>
|
||||
</MessageBanner>
|
||||
<section class="universal-card">
|
||||
<h2>Themes</h2>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme-selector">
|
||||
<span class="label__title">Color theme</span>
|
||||
<span class="label__description">Change the global site color theme.</span>
|
||||
</label>
|
||||
<div>
|
||||
<Multiselect
|
||||
id="theme-selector"
|
||||
v-model="$colorMode.preference"
|
||||
:options="['system', 'light', 'dark', 'oled']"
|
||||
:custom-label="
|
||||
(value) =>
|
||||
value === 'oled' ? 'OLED' : value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@update:model-value="(value) => updateTheme(value, true)"
|
||||
/>
|
||||
<h2>{{ formatMessage(colorTheme.title) }}</h2>
|
||||
<p>{{ formatMessage(colorTheme.description) }}</p>
|
||||
<div class="theme-options">
|
||||
<button
|
||||
v-for="option in themeOptions"
|
||||
:key="option"
|
||||
class="preview-radio button-base"
|
||||
:class="{ selected: theme.preference === option }"
|
||||
@click="() => updateColorTheme(option)"
|
||||
>
|
||||
<div class="preview" :class="`${option === 'system' ? systemTheme : option}-mode`">
|
||||
<div class="example-card card card">
|
||||
<div class="example-icon"></div>
|
||||
<div class="example-text-1"></div>
|
||||
<div class="example-text-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">
|
||||
<RadioButtonChecked v-if="theme.preference === option" class="radio" />
|
||||
<RadioButtonIcon v-else class="radio" />
|
||||
{{ colorTheme[option] ? formatMessage(colorTheme[option]) : option }}
|
||||
<SunIcon
|
||||
v-if="'light' === option"
|
||||
v-tooltip="formatMessage(colorTheme.preferredLight)"
|
||||
class="theme-icon"
|
||||
/>
|
||||
<MoonIcon
|
||||
v-else-if="cosmetics.preferredDarkTheme === option"
|
||||
v-tooltip="formatMessage(colorTheme.preferredDark)"
|
||||
class="theme-icon"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>{{ formatMessage(projectListLayouts.title) }}</h2>
|
||||
<p>{{ formatMessage(projectListLayouts.description) }}</p>
|
||||
<div class="project-lists">
|
||||
<div v-for="projectType in listTypes" :key="projectType.id + '-project-list-layouts'">
|
||||
<div class="label">
|
||||
<div class="label__title">
|
||||
{{
|
||||
projectListLayouts[projectType.id]
|
||||
? formatMessage(projectListLayouts[projectType.id])
|
||||
: projectType.id
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-list-layouts">
|
||||
<button
|
||||
class="preview-radio button-base"
|
||||
:class="{ selected: cosmetics.searchDisplayMode[projectType.id] === 'list' }"
|
||||
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'list')"
|
||||
>
|
||||
<div class="preview">
|
||||
<div class="layout-list-mode">
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">
|
||||
<RadioButtonChecked
|
||||
v-if="cosmetics.searchDisplayMode[projectType.id] === 'list'"
|
||||
class="radio"
|
||||
/>
|
||||
<RadioButtonIcon v-else class="radio" />
|
||||
Rows
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="preview-radio button-base"
|
||||
:class="{ selected: cosmetics.searchDisplayMode[projectType.id] === 'grid' }"
|
||||
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'grid')"
|
||||
>
|
||||
<div class="preview">
|
||||
<div class="layout-grid-mode">
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">
|
||||
<RadioButtonChecked
|
||||
v-if="cosmetics.searchDisplayMode[projectType.id] === 'grid'"
|
||||
class="radio"
|
||||
/>
|
||||
<RadioButtonIcon v-else class="radio" />
|
||||
Grid
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="preview-radio button-base"
|
||||
:class="{ selected: cosmetics.searchDisplayMode[projectType.id] === 'gallery' }"
|
||||
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'gallery')"
|
||||
>
|
||||
<div class="preview">
|
||||
<div class="layout-gallery-mode">
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
<div class="example-card card"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">
|
||||
<RadioButtonChecked
|
||||
v-if="cosmetics.searchDisplayMode[projectType.id] === 'gallery'"
|
||||
class="radio"
|
||||
/>
|
||||
<RadioButtonIcon v-else class="radio" />
|
||||
Gallery
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input small">
|
||||
<label for="search-layout-toggle">
|
||||
<span class="label__title">Search sidebar on the right</span>
|
||||
<span class="label__description"
|
||||
>Enabling this will put the search page's filters sidebar on the right side.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="search-layout-toggle"
|
||||
v-model="cosmetics.searchLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="project-layout-toggle">
|
||||
<span class="label__title">Project sidebar on the right</span>
|
||||
<span class="label__description"
|
||||
>Enabling this will put the project pages' info sidebars on the right side.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="project-layout-toggle"
|
||||
v-model="cosmetics.projectLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>Project list display mode</h2>
|
||||
<div
|
||||
v-for="projectType in listTypes"
|
||||
:key="projectType.id + '-display-mode-selector'"
|
||||
class="adjacent-input"
|
||||
>
|
||||
<label :for="projectType.id + '-search-display-mode'">
|
||||
<span class="label__title">{{ projectType.name }} display mode</span>
|
||||
<span class="label__description"
|
||||
>Change the display view for {{ projectType.display }}.</span
|
||||
>
|
||||
</label>
|
||||
<Multiselect
|
||||
:id="projectType + '-search-display-mode'"
|
||||
v-model="cosmetics.searchDisplayMode[projectType.id]"
|
||||
:options="tags.projectViewModes"
|
||||
:custom-label="$capitalizeString"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@update:model-value="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>Feature flags</h2>
|
||||
<h2>{{ formatMessage(featureFlags.title) }}</h2>
|
||||
<p>{{ formatMessage(featureFlags.description) }}</p>
|
||||
<div class="adjacent-input small">
|
||||
<label for="advanced-rendering">
|
||||
<span class="label__title">Advanced rendering</span>
|
||||
<span class="label__description"
|
||||
>Enables advanced rendering such as blur effects that may cause performance issues
|
||||
without hardware-accelerated rendering.</span
|
||||
>
|
||||
<span class="label__title">
|
||||
{{ formatMessage(featureFlags.advancedRenderingTitle) }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(featureFlags.advancedRenderingDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="advanced-rendering"
|
||||
@@ -102,11 +158,11 @@
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="external-links-new-tab">
|
||||
<span class="label__title">Open external links in new tab</span>
|
||||
<span class="label__title">
|
||||
{{ formatMessage(featureFlags.externalLinksNewTabTitle) }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
Make links which go outside of Modrinth open in a new tab. No matter this setting, links
|
||||
on the same domain and in Markdown descriptions will open in the same tab, and links on
|
||||
ads and edit pages will open in a new tab.
|
||||
{{ formatMessage(featureFlags.externalLinksNewTabDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -119,10 +175,11 @@
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="modrinth-app-promos">
|
||||
<span class="label__title">Hide Modrinth App promotions</span>
|
||||
<span class="label__title">
|
||||
{{ formatMessage(featureFlags.hideModrinthAppPromosTitle) }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
Hides the "Get Modrinth App" buttons from primary navigation. The Modrinth App page can
|
||||
still be found on the landing page or in the footer.
|
||||
{{ formatMessage(featureFlags.hideModrinthAppPromosDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -133,34 +190,447 @@
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="search-layout-toggle">
|
||||
<span class="label__title">
|
||||
{{ formatMessage(featureFlags.rightAlignedSearchSidebarTitle) }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(featureFlags.rightAlignedSearchSidebarDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="search-layout-toggle"
|
||||
v-model="cosmetics.searchLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="project-layout-toggle">
|
||||
<span class="label__title">
|
||||
{{ formatMessage(featureFlags.rightAlignedProjectSidebarTitle) }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(featureFlags.rightAlignedProjectSidebarDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-layout-toggle"
|
||||
v-model="cosmetics.projectLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import { CodeIcon, Button, RadioButtonIcon, RadioButtonChecked, SunIcon, MoonIcon } from 'omorphia'
|
||||
import { formatProjectType } from '~/plugins/shorthands.js'
|
||||
import MessageBanner from '~/components/ui/MessageBanner.vue'
|
||||
import { DARK_THEMES } from '~/composables/theme.js'
|
||||
|
||||
useHead({
|
||||
title: 'Display settings - Modrinth',
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const developerModeBanner = defineMessages({
|
||||
description: {
|
||||
id: 'settings.display.banner.developer-mode.description',
|
||||
defaultMessage:
|
||||
"<strong>Developer mode</strong> is active. This will allow you to view the internal IDs of various things throughout Modrinth that may be helpful if you're a developer using the Modrinth API. Click on the Modrinth logo at the bottom of the page 5 times to toggle developer mode.",
|
||||
},
|
||||
deactivate: {
|
||||
id: 'settings.display.banner.developer-mode.button',
|
||||
defaultMessage: 'Deactivate developer mode',
|
||||
},
|
||||
})
|
||||
|
||||
const colorTheme = defineMessages({
|
||||
title: {
|
||||
id: 'settings.display.theme.title',
|
||||
defaultMessage: 'Color theme',
|
||||
},
|
||||
description: {
|
||||
id: 'settings.display.theme.description',
|
||||
defaultMessage: 'Select your preferred color theme for Modrinth on this device.',
|
||||
},
|
||||
system: {
|
||||
id: 'settings.display.theme.system',
|
||||
defaultMessage: 'Sync with system',
|
||||
},
|
||||
light: {
|
||||
id: 'settings.display.theme.light',
|
||||
defaultMessage: 'Light',
|
||||
},
|
||||
dark: {
|
||||
id: 'settings.display.theme.dark',
|
||||
defaultMessage: 'Dark',
|
||||
},
|
||||
oled: {
|
||||
id: 'settings.display.theme.oled',
|
||||
defaultMessage: 'OLED',
|
||||
},
|
||||
retro: {
|
||||
id: 'settings.display.theme.retro',
|
||||
defaultMessage: 'Retro',
|
||||
},
|
||||
preferredLight: {
|
||||
id: 'settings.display.theme.preferred-light-theme',
|
||||
defaultMessage: 'Preferred light theme',
|
||||
},
|
||||
preferredDark: {
|
||||
id: 'settings.display.theme.preferred-dark-theme',
|
||||
defaultMessage: 'Preferred dark theme',
|
||||
},
|
||||
})
|
||||
|
||||
const projectListLayouts = defineMessages({
|
||||
title: {
|
||||
id: 'settings.display.project-list-layouts.title',
|
||||
defaultMessage: 'Project list layouts',
|
||||
},
|
||||
description: {
|
||||
id: 'settings.display.project-list-layouts.description',
|
||||
defaultMessage:
|
||||
'Select your preferred layout for each page that displays project lists on this device.',
|
||||
},
|
||||
mod: {
|
||||
id: 'settings.display.project-list-layouts.mod',
|
||||
defaultMessage: 'Mods page',
|
||||
},
|
||||
plugin: {
|
||||
id: 'settings.display.project-list-layouts.plugin',
|
||||
defaultMessage: 'Plugins page',
|
||||
},
|
||||
datapack: {
|
||||
id: 'settings.display.project-list-layouts.datapack',
|
||||
defaultMessage: 'Data Packs page',
|
||||
},
|
||||
shader: {
|
||||
id: 'settings.display.project-list-layouts.shader',
|
||||
defaultMessage: 'Shaders page',
|
||||
},
|
||||
resourcepack: {
|
||||
id: 'settings.display.project-list-layouts.resourcepack',
|
||||
defaultMessage: 'Resource Packs page',
|
||||
},
|
||||
modpack: {
|
||||
id: 'settings.display.project-list-layouts.modpack',
|
||||
defaultMessage: 'Modpacks page',
|
||||
},
|
||||
user: {
|
||||
id: 'settings.display.project-list-layouts.user',
|
||||
defaultMessage: 'User profile pages',
|
||||
},
|
||||
})
|
||||
|
||||
const featureFlags = defineMessages({
|
||||
title: {
|
||||
id: 'settings.display.flags.title',
|
||||
defaultMessage: 'Feature flags',
|
||||
},
|
||||
description: {
|
||||
id: 'settings.display.flags.description',
|
||||
defaultMessage: 'Enable or disable certain features on this device.',
|
||||
},
|
||||
advancedRenderingTitle: {
|
||||
id: 'settings.display.sidebar.advanced-rendering.title',
|
||||
defaultMessage: 'Advanced rendering',
|
||||
},
|
||||
advancedRenderingDescription: {
|
||||
id: 'settings.display.sidebar.advanced-rendering.description',
|
||||
defaultMessage:
|
||||
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
|
||||
},
|
||||
externalLinksNewTabTitle: {
|
||||
id: 'settings.display.sidebar.external-links-new-tab.title',
|
||||
defaultMessage: 'Open external links in new tab',
|
||||
},
|
||||
externalLinksNewTabDescription: {
|
||||
id: 'settings.display.sidebar.external-links-new-tab.description',
|
||||
defaultMessage:
|
||||
'Make links which go outside of Modrinth open in a new tab. No matter this setting, links on the same domain and in Markdown descriptions will open in the same tab, and links on ads and edit pages will open in a new tab.',
|
||||
},
|
||||
hideModrinthAppPromosTitle: {
|
||||
id: 'settings.display.sidebar.hide-app-promos.title',
|
||||
defaultMessage: 'Hide Modrinth App promotions',
|
||||
},
|
||||
hideModrinthAppPromosDescription: {
|
||||
id: 'settings.display.sidebar.hide-app-promos.description',
|
||||
defaultMessage:
|
||||
'Hides the "Get Modrinth App" buttons from primary navigation. The Modrinth App page can still be found on the landing page or in the footer.',
|
||||
},
|
||||
rightAlignedSearchSidebarTitle: {
|
||||
id: 'settings.display.sidebar.right-aligned-search-sidebar.title',
|
||||
defaultMessage: 'Right-aligned search sidebar',
|
||||
},
|
||||
rightAlignedSearchSidebarDescription: {
|
||||
id: 'settings.display.sidebar.right-aligned-search-sidebar.description',
|
||||
defaultMessage: 'Aligns the search filters sidebar to the right of the search results.',
|
||||
},
|
||||
rightAlignedProjectSidebarTitle: {
|
||||
id: 'settings.display.sidebar.right-aligned-project-sidebar.title',
|
||||
defaultMessage: 'Right-aligned project sidebar',
|
||||
},
|
||||
rightAlignedProjectSidebarDescription: {
|
||||
id: 'settings.display.sidebar.right-aligned-project-sidebar.description',
|
||||
defaultMessage: "Aligns the project details sidebar to the right of the page's content.",
|
||||
},
|
||||
})
|
||||
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useTags()
|
||||
|
||||
const systemTheme = ref('light')
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
const themeOptions = computed(() => {
|
||||
const options = ['system', 'light', 'dark', 'oled']
|
||||
if (cosmetics.value.developerMode || theme.value.preference === 'retro') {
|
||||
options.push('retro')
|
||||
}
|
||||
return options
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateSystemTheme()
|
||||
})
|
||||
|
||||
function updateSystemTheme() {
|
||||
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: light)')
|
||||
if (colorSchemeQueryList.matches) {
|
||||
systemTheme.value = 'light'
|
||||
} else {
|
||||
systemTheme.value = cosmetics.value.preferredDarkTheme
|
||||
}
|
||||
}
|
||||
|
||||
function updateColorTheme(value) {
|
||||
if (DARK_THEMES.includes(value)) {
|
||||
cosmetics.value.preferredDarkTheme = value
|
||||
saveCosmetics()
|
||||
updateSystemTheme()
|
||||
}
|
||||
updateTheme(value, true)
|
||||
}
|
||||
|
||||
function disableDeveloperMode() {
|
||||
cosmetics.value.developerMode = !cosmetics.value.developerMode
|
||||
saveCosmetics()
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'Developer mode deactivated',
|
||||
text: 'Developer mode has been disabled',
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
|
||||
const listTypes = computed(() => {
|
||||
const types = tags.value.projectTypes.map((type) => {
|
||||
return {
|
||||
id: type.id,
|
||||
name: formatProjectType(type.id) + ' search',
|
||||
name: formatProjectType(type.id) + 's',
|
||||
display: 'the ' + formatProjectType(type.id).toLowerCase() + 's search page',
|
||||
}
|
||||
})
|
||||
types.push({
|
||||
id: 'user',
|
||||
name: 'User page',
|
||||
name: 'User profiles',
|
||||
display: 'user pages',
|
||||
})
|
||||
return types
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.preview-radio {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-divider);
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
&.selected {
|
||||
color: var(--color-contrast);
|
||||
|
||||
.label {
|
||||
.radio {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
background-color: var(--color-bg);
|
||||
padding: 1.5rem;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
.example-card {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
outline: 2px solid transparent;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
padding: var(--gap-md) var(--gap-lg);
|
||||
|
||||
.radio {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
color: var(--color-secondary);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.theme-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
|
||||
gap: var(--gap-lg);
|
||||
|
||||
.preview .example-card {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template: 'icon text1' 'icon text2';
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.5rem;
|
||||
outline: 2px solid transparent;
|
||||
|
||||
.example-icon {
|
||||
grid-area: icon;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
outline: 2px solid transparent;
|
||||
}
|
||||
|
||||
.example-text-1,
|
||||
.example-text-2 {
|
||||
height: 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
outline: 2px solid transparent;
|
||||
}
|
||||
|
||||
.example-text-1 {
|
||||
grid-area: text1;
|
||||
width: 100%;
|
||||
background-color: var(--color-base);
|
||||
}
|
||||
|
||||
.example-text-2 {
|
||||
grid-area: text2;
|
||||
width: 60%;
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-lists {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
|
||||
> :first-child .label__title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
--_layout-width: 7rem;
|
||||
--_layout-height: 4.5rem;
|
||||
--_layout-gap: 0.25rem;
|
||||
|
||||
.example-card {
|
||||
border-radius: 0.5rem;
|
||||
width: var(--_layout-width);
|
||||
height: calc((var(--_layout-height) - 3 * var(--_layout-gap)) / 4);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-list-mode {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--_layout-gap);
|
||||
}
|
||||
|
||||
.layout-grid-mode {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: var(--_layout-gap);
|
||||
|
||||
.example-card {
|
||||
width: calc((var(--_layout-width) - 2 * var(--_layout-gap)) / 3);
|
||||
height: calc((var(--_layout-height) - var(--_layout-gap)) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-gallery-mode {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--_layout-gap);
|
||||
|
||||
.example-card {
|
||||
width: calc((var(--_layout-width) - var(--_layout-gap)) / 2);
|
||||
height: calc((var(--_layout-height) - var(--_layout-gap)) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-list-layouts {
|
||||
display: flex;
|
||||
gap: var(--gap-lg);
|
||||
width: fit-content;
|
||||
|
||||
.preview-radio .example-card {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.preview-radio.selected .example-card {
|
||||
border-color: var(--color-brand);
|
||||
background-color: var(--color-brand-highlight);
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.developer-message {
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 2px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: var(--gap-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
<div class="header__row">
|
||||
<div class="header__title">
|
||||
<h2>{{ formatMessage(messages.longTitle) }}</h2>
|
||||
<h2>{{ formatMessage(messages.title) }}</h2>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@@ -264,11 +264,7 @@ const deleteModalMessages = defineMessages({
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'settings.pats.title',
|
||||
defaultMessage: 'PATs',
|
||||
},
|
||||
longTitle: {
|
||||
id: 'settings.pats.title.long',
|
||||
defaultMessage: 'Personal Access Tokens',
|
||||
defaultMessage: 'Personal access tokens',
|
||||
},
|
||||
description: {
|
||||
id: 'settings.pats.description',
|
||||
|
||||
246
pages/settings/profile.vue
Normal file
246
pages/settings/profile.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="card">
|
||||
<h2>{{ formatMessage(messages.title) }}</h2>
|
||||
<p>
|
||||
<IntlFormatted :message-id="messages.description">
|
||||
<template #docs-link="{ children }">
|
||||
<a href="https://docs.modrinth.com/" target="_blank" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
<label>
|
||||
<span class="label__title">{{ formatMessage(messages.profilePicture) }}</span>
|
||||
</label>
|
||||
<div class="avatar-changer">
|
||||
<Avatar
|
||||
:src="previewImage ? previewImage : avatarUrl"
|
||||
size="md"
|
||||
circle
|
||||
:alt="auth.user.username"
|
||||
/>
|
||||
<div class="input-stack">
|
||||
<FileInput
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
class="btn"
|
||||
:prompt="formatMessage(commonMessages.uploadImageButton)"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<Button
|
||||
v-if="previewImage"
|
||||
:action="
|
||||
() => {
|
||||
icon = null
|
||||
previewImage = null
|
||||
}
|
||||
"
|
||||
>
|
||||
<UndoIcon />
|
||||
{{ formatMessage(messages.profilePictureReset) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<label for="username-field">
|
||||
<span class="label__title">{{ formatMessage(messages.usernameTitle) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.usernameDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input id="username-field" v-model="username" type="text" />
|
||||
<label for="bio-field">
|
||||
<span class="label__title">{{ formatMessage(messages.bioTitle) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.bioDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<textarea id="bio-field" v-model="bio" type="text" />
|
||||
<div v-if="hasUnsavedChanges" class="input-group">
|
||||
<Button color="primary" :action="() => saveChanges()">
|
||||
<SaveIcon /> {{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</Button>
|
||||
<Button :action="() => cancel()">
|
||||
<XIcon /> {{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="input-group">
|
||||
<Button disabled color="primary" :action="() => saveChanges()">
|
||||
<SaveIcon />
|
||||
{{
|
||||
saved
|
||||
? formatMessage(commonMessages.changesSavedLabel)
|
||||
: formatMessage(commonMessages.saveChangesButton)
|
||||
}}
|
||||
</Button>
|
||||
<Button :link="`/user/${auth.user.username}`">
|
||||
<UserIcon /> {{ formatMessage(messages.visitProfile) }}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Button,
|
||||
UserIcon,
|
||||
SaveIcon,
|
||||
Avatar,
|
||||
FileInput,
|
||||
UploadIcon,
|
||||
UndoIcon,
|
||||
XIcon,
|
||||
} from 'omorphia'
|
||||
import { commonMessages } from '~/utils/common-messages'
|
||||
|
||||
useHead({
|
||||
title: 'Account settings - Modrinth',
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'settings.profile.profile-info',
|
||||
defaultMessage: 'Profile information',
|
||||
},
|
||||
description: {
|
||||
id: 'settings.profile.description',
|
||||
defaultMessage:
|
||||
'Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>.',
|
||||
},
|
||||
profilePicture: {
|
||||
id: 'settings.profile.profile-picture.title',
|
||||
defaultMessage: 'Profile picture',
|
||||
},
|
||||
profilePictureReset: {
|
||||
id: 'settings.profile.profile-picture.reset',
|
||||
defaultMessage: 'Reset',
|
||||
},
|
||||
usernameTitle: {
|
||||
id: 'settings.profile.username.title',
|
||||
defaultMessage: 'Username',
|
||||
},
|
||||
usernameDescription: {
|
||||
id: 'settings.profile.username.description',
|
||||
defaultMessage: 'A unique case-insensitive name to identify your profile.',
|
||||
},
|
||||
bioTitle: {
|
||||
id: 'settings.profile.bio.title',
|
||||
defaultMessage: 'Bio',
|
||||
},
|
||||
bioDescription: {
|
||||
id: 'settings.profile.bio.description',
|
||||
defaultMessage: 'A short description to tell everyone a little bit about you.',
|
||||
},
|
||||
visitProfile: {
|
||||
id: 'settings.profile.visit-profile',
|
||||
defaultMessage: 'Visit your profile',
|
||||
},
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
const username = ref(auth.value.user.username)
|
||||
const bio = ref(auth.value.user.bio)
|
||||
const avatarUrl = ref(auth.value.user.avatar_url)
|
||||
const icon = shallowRef(null)
|
||||
const previewImage = shallowRef(null)
|
||||
const saved = ref(false)
|
||||
|
||||
const hasUnsavedChanges = computed(
|
||||
() =>
|
||||
username.value !== auth.value.user.username ||
|
||||
bio.value !== auth.value.user.bio ||
|
||||
previewImage.value
|
||||
)
|
||||
|
||||
function showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
icon.value = files[0]
|
||||
reader.readAsDataURL(icon.value)
|
||||
reader.onload = (event) => {
|
||||
previewImage.value = event.target.result
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
icon.value = null
|
||||
previewImage.value = null
|
||||
username.value = auth.value.user.username
|
||||
bio.value = auth.value.user.bio
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
startLoading()
|
||||
try {
|
||||
if (icon.value) {
|
||||
await useBaseFetch(
|
||||
`user/${auth.value.user.id}/icon?ext=${
|
||||
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
|
||||
}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: icon.value,
|
||||
}
|
||||
)
|
||||
icon.value = null
|
||||
previewImage.value = null
|
||||
}
|
||||
|
||||
const body = {}
|
||||
|
||||
if (auth.value.user.username !== username.value) {
|
||||
body.username = username.value
|
||||
}
|
||||
|
||||
if (auth.value.user.bio !== bio.value) {
|
||||
body.bio = bio.value
|
||||
}
|
||||
|
||||
await useBaseFetch(`user/${auth.value.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body,
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
avatarUrl.value = auth.value.user.avatar_url
|
||||
saved.value = true
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err
|
||||
? err.data
|
||||
? err.data.description
|
||||
? err.data.description
|
||||
: err.data
|
||||
: err
|
||||
: 'aaaaahhh',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.avatar-changer {
|
||||
display: flex;
|
||||
gap: var(--gap-lg);
|
||||
margin-top: var(--gap-md);
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 6rem;
|
||||
width: 40rem;
|
||||
margin-bottom: var(--gap-lg);
|
||||
}
|
||||
</style>
|
||||
@@ -4,12 +4,7 @@
|
||||
<CollectionCreateModal ref="modal_collection_creation" />
|
||||
<div class="user-header-wrapper">
|
||||
<div class="user-header">
|
||||
<Avatar
|
||||
:src="previewImage ? previewImage : user.avatar_url"
|
||||
size="md"
|
||||
circle
|
||||
:alt="user.username"
|
||||
/>
|
||||
<Avatar :src="user.avatar_url" size="md" circle :alt="user.username" />
|
||||
<h1 class="username">
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
@@ -22,25 +17,14 @@
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
<div class="card__overlay">
|
||||
<FileInput
|
||||
v-if="isEditing"
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
:prompt="formatMessage(messages.profileUploadAvatarInput)"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image iconified-button"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<button
|
||||
v-else-if="auth.user && auth.user.id === user.id"
|
||||
<NuxtLink
|
||||
v-if="auth.user && auth.user.id === user.id"
|
||||
to="/settings/profile"
|
||||
class="iconified-button"
|
||||
@click="isEditing = true"
|
||||
>
|
||||
<EditIcon />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else-if="auth.user"
|
||||
class="iconified-button"
|
||||
@@ -54,123 +38,85 @@
|
||||
{{ formatMessage(messages.profileReportButton) }}
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<template v-if="isEditing">
|
||||
<div class="inputs universal-labels">
|
||||
<label for="user-username">
|
||||
<span class="label__title">
|
||||
{{ formatMessage(messages.profileEditUsernameLabel) }}
|
||||
</span>
|
||||
</label>
|
||||
<input id="user-username" v-model="user.username" maxlength="39" type="text" />
|
||||
<label for="user-bio">
|
||||
<span class="label__title">
|
||||
{{ formatMessage(messages.profileEditBioLabel) }}
|
||||
</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea id="user-bio" v-model="user.bio" maxlength="160" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
() => {
|
||||
isEditing = false
|
||||
user = JSON.parse(JSON.stringify(auth.user))
|
||||
previewImage = null
|
||||
icon = null
|
||||
}
|
||||
"
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="tags.staffRoles.includes(user.role)" :type="user.role" />
|
||||
<Badge v-else-if="projects.length > 0" type="creator" />
|
||||
</div>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileDownloadsStats"
|
||||
:values="{ count: formatCompactNumber(sumDownloads) }"
|
||||
>
|
||||
<CrossIcon /> {{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="saveChanges">
|
||||
<SaveIcon /> {{ formatMessage(commonMessages.saveButton) }}
|
||||
</button>
|
||||
<template #stat="{ children }">
|
||||
<span class="primary-stat__counter">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="tags.staffRoles.includes(user.role)" :type="user.role" />
|
||||
<Badge v-else-if="projects.length > 0" type="creator" />
|
||||
</div>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileDownloadsStats"
|
||||
:values="{ count: formatCompactNumber(sumDownloads) }"
|
||||
>
|
||||
<template #stat="{ children }">
|
||||
<span class="primary-stat__counter">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileProjectsFollowersStats"
|
||||
:values="{ count: formatCompactNumber(sumFollows) }"
|
||||
>
|
||||
<template #stat="{ children }">
|
||||
<span class="primary-stat__counter">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="
|
||||
formatMessage(commonMessages.dateAtTimeTooltip, {
|
||||
date: new Date(user.created),
|
||||
time: new Date(user.created),
|
||||
})
|
||||
"
|
||||
class="secondary-stat__text date"
|
||||
</div>
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileProjectsFollowersStats"
|
||||
:values="{ count: formatCompactNumber(sumFollows) }"
|
||||
>
|
||||
{{
|
||||
formatMessage(messages.profileJoinedAt, { ago: formatRelativeTime(user.created) })
|
||||
}}
|
||||
</span>
|
||||
<template #stat="{ children }">
|
||||
<span class="primary-stat__counter">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="
|
||||
formatMessage(commonMessages.dateAtTimeTooltip, {
|
||||
date: new Date(user.created),
|
||||
time: new Date(user.created),
|
||||
})
|
||||
"
|
||||
class="secondary-stat__text date"
|
||||
>
|
||||
{{
|
||||
formatMessage(messages.profileJoinedAt, { ago: formatRelativeTime(user.created) })
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text">
|
||||
<IntlFormatted :message-id="messages.profileUserId">
|
||||
<template #~id>
|
||||
<CopyCode :text="user.id" />
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="organizations.length > 0">
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text">
|
||||
<IntlFormatted :message-id="messages.profileUserId">
|
||||
<template #~id>
|
||||
<CopyCode :text="user.id" />
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="organizations.length > 0">
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item">
|
||||
<IntlFormatted :message-id="messages.profileOrganizations" />
|
||||
<div class="organizations-grid">
|
||||
<nuxt-link
|
||||
v-for="org in organizations"
|
||||
:key="org.id"
|
||||
v-tooltip="org.name"
|
||||
class="organization"
|
||||
:to="`/organization/${org.slug}`"
|
||||
>
|
||||
<Avatar :src="org.icon_url" :alt="'Icon for ' + org.name" size="xs" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="stats-block__item">
|
||||
<IntlFormatted :message-id="messages.profileOrganizations" />
|
||||
<div class="organizations-grid">
|
||||
<nuxt-link
|
||||
v-for="org in organizations"
|
||||
:key="org.id"
|
||||
v-tooltip="org.name"
|
||||
class="organization"
|
||||
:to="`/organization/${org.slug}`"
|
||||
>
|
||||
<Avatar :src="org.icon_url" :alt="'Icon for ' + org.name" size="xs" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,14 +293,10 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import GridIcon from '~/assets/images/utils/grid.svg'
|
||||
import ListIcon from '~/assets/images/utils/list.svg'
|
||||
import ImageIcon from '~/assets/images/utils/image.svg'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||
import WorldIcon from '~/assets/images/utils/world.svg'
|
||||
import FileInput from '~/components/ui/FileInput.vue'
|
||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||
import NavRow from '~/components/ui/NavRow.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
@@ -553,61 +495,6 @@ const sumFollows = computed(() => {
|
||||
return sum
|
||||
})
|
||||
|
||||
const isEditing = ref(false)
|
||||
const icon = shallowRef(null)
|
||||
const previewImage = shallowRef(null)
|
||||
|
||||
function showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
icon.value = files[0]
|
||||
reader.readAsDataURL(icon.value)
|
||||
reader.onload = (event) => {
|
||||
previewImage.value = event.target.result
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
startLoading()
|
||||
try {
|
||||
if (icon.value) {
|
||||
await useBaseFetch(
|
||||
`user/${auth.value.user.id}/icon?ext=${
|
||||
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
|
||||
}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: icon.value,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const reqData = {
|
||||
email: user.value.email,
|
||||
bio: user.value.bio,
|
||||
}
|
||||
if (user.value.username !== auth.value.user.username) {
|
||||
reqData.username = user.value.username
|
||||
}
|
||||
|
||||
await useBaseFetch(`user/${auth.value.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: reqData,
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
|
||||
isEditing.value = false
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
function cycleSearchDisplayMode() {
|
||||
cosmetics.value.searchDisplayMode.user = data.$cycleValue(
|
||||
cosmetics.value.searchDisplayMode.user,
|
||||
|
||||
@@ -15,6 +15,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'button.continue',
|
||||
defaultMessage: 'Continue',
|
||||
},
|
||||
changesSavedLabel: {
|
||||
id: 'label.changes-saved',
|
||||
defaultMessage: 'Changes saved',
|
||||
},
|
||||
createAProjectButton: {
|
||||
id: 'button.create-a-project',
|
||||
defaultMessage: 'Create a project',
|
||||
@@ -65,7 +69,7 @@ export const commonMessages = defineMessages({
|
||||
},
|
||||
listInputView: {
|
||||
id: 'input.view.list',
|
||||
defaultMessage: 'List view',
|
||||
defaultMessage: 'Rows view',
|
||||
},
|
||||
moderationLabel: {
|
||||
id: 'label.moderation',
|
||||
@@ -123,6 +127,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.unlisted',
|
||||
defaultMessage: 'Unlisted',
|
||||
},
|
||||
uploadImageButton: {
|
||||
id: 'button.upload-image',
|
||||
defaultMessage: 'Upload image',
|
||||
},
|
||||
visibilityLabel: {
|
||||
id: 'label.visibility',
|
||||
defaultMessage: 'Visibility',
|
||||
|
||||
Reference in New Issue
Block a user