Add TailwindCSS (#1252)

* Setup TailwindCSS

* Fully setup configuration

* Refactor some tailwind variables
This commit is contained in:
Evan Song
2024-07-06 20:57:32 -07:00
committed by GitHub
parent 0f2ddb452c
commit abec2e48d4
176 changed files with 7905 additions and 7433 deletions

View File

@@ -179,7 +179,7 @@
<qrcode-vue
v-if="twoFactorSecret"
:value="`otpauth://totp/${encodeURIComponent(
auth.user.email
auth.user.email,
)}?secret=${twoFactorSecret}&issuer=Modrinth`"
:size="250"
:margin="2"
@@ -255,15 +255,15 @@
<Modal ref="manageProvidersModal" header="Authentication providers">
<div class="universal-modal">
<div class="table">
<div class="table-row table-head">
<div class="table-cell table-text">Provider</div>
<div class="table-cell table-text">Actions</div>
<div class="table-head table-row">
<div class="table-text table-cell">Provider</div>
<div class="table-text table-cell">Actions</div>
</div>
<div v-for="provider in authProviders" :key="provider.id" class="table-row">
<div class="table-cell table-text">
<div class="table-text table-cell">
<span><component :is="provider.icon" /> {{ provider.display }}</span>
</div>
<div class="table-cell table-text manage">
<div class="table-text manage table-cell">
<button
v-if="auth.user.auth_providers.includes(provider.id)"
class="btn"
@@ -327,11 +327,11 @@
class="iconified-button"
@click="
() => {
oldPassword = ''
newPassword = ''
confirmNewPassword = ''
removePasswordMode = false
$refs.managePasswordModal.show()
oldPassword = '';
newPassword = '';
confirmNewPassword = '';
removePasswordMode = false;
$refs.managePasswordModal.show();
}
"
>
@@ -401,218 +401,218 @@ import {
RightArrowIcon,
CheckIcon,
ExternalIcon,
} from '@modrinth/assets'
import QrcodeVue from 'qrcode.vue'
import GitHubIcon from 'assets/icons/auth/sso-github.svg'
import MicrosoftIcon from 'assets/icons/auth/sso-microsoft.svg'
import GoogleIcon from 'assets/icons/auth/sso-google.svg'
import SteamIcon from 'assets/icons/auth/sso-steam.svg'
import DiscordIcon from 'assets/icons/auth/sso-discord.svg'
import KeyIcon from 'assets/icons/auth/key.svg'
import GitLabIcon from 'assets/icons/auth/sso-gitlab.svg'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import Modal from '~/components/ui/Modal.vue'
} from "@modrinth/assets";
import QrcodeVue from "qrcode.vue";
import GitHubIcon from "assets/icons/auth/sso-github.svg";
import MicrosoftIcon from "assets/icons/auth/sso-microsoft.svg";
import GoogleIcon from "assets/icons/auth/sso-google.svg";
import SteamIcon from "assets/icons/auth/sso-steam.svg";
import DiscordIcon from "assets/icons/auth/sso-discord.svg";
import KeyIcon from "assets/icons/auth/key.svg";
import GitLabIcon from "assets/icons/auth/sso-gitlab.svg";
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
import Modal from "~/components/ui/Modal.vue";
useHead({
title: 'Account settings - Modrinth',
})
title: "Account settings - Modrinth",
});
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
const data = useNuxtApp()
const auth = await useAuth()
const data = useNuxtApp();
const auth = await useAuth();
const changeEmailModal = ref()
const email = ref(auth.value.user.email)
const changeEmailModal = ref();
const email = ref(auth.value.user.email);
async function saveEmail() {
if (!email.value) {
return
return;
}
startLoading()
startLoading();
try {
await useBaseFetch(`auth/email`, {
method: 'PATCH',
method: "PATCH",
body: {
email: email.value,
},
})
changeEmailModal.value.hide()
await useAuth(auth.value.token)
});
changeEmailModal.value.hide();
await useAuth(auth.value.token);
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
const managePasswordModal = ref()
const removePasswordMode = ref(false)
const oldPassword = ref('')
const newPassword = ref('')
const confirmNewPassword = ref('')
const managePasswordModal = ref();
const removePasswordMode = ref(false);
const oldPassword = ref("");
const newPassword = ref("");
const confirmNewPassword = ref("");
async function savePassword() {
if (newPassword.value !== confirmNewPassword.value) {
return
return;
}
startLoading()
startLoading();
try {
await useBaseFetch(`auth/password`, {
method: 'PATCH',
method: "PATCH",
body: {
old_password: auth.value.user.has_password ? oldPassword.value : null,
new_password: removePasswordMode.value ? null : newPassword.value,
},
})
managePasswordModal.value.hide()
await useAuth(auth.value.token)
});
managePasswordModal.value.hide();
await useAuth(auth.value.token);
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
const manageTwoFactorModal = ref()
const twoFactorSecret = ref(null)
const twoFactorFlow = ref(null)
const twoFactorStep = ref(0)
const manageTwoFactorModal = ref();
const twoFactorSecret = ref(null);
const twoFactorFlow = ref(null);
const twoFactorStep = ref(0);
async function showTwoFactorModal() {
twoFactorStep.value = 0
twoFactorCode.value = null
twoFactorIncorrect.value = false
twoFactorStep.value = 0;
twoFactorCode.value = null;
twoFactorIncorrect.value = false;
if (auth.value.user.has_totp) {
manageTwoFactorModal.value.show()
return
manageTwoFactorModal.value.show();
return;
}
twoFactorSecret.value = null
twoFactorFlow.value = null
backupCodes.value = []
manageTwoFactorModal.value.show()
twoFactorSecret.value = null;
twoFactorFlow.value = null;
backupCodes.value = [];
manageTwoFactorModal.value.show();
startLoading()
startLoading();
try {
const res = await useBaseFetch('auth/2fa/get_secret', {
method: 'POST',
})
const res = await useBaseFetch("auth/2fa/get_secret", {
method: "POST",
});
twoFactorSecret.value = res.secret
twoFactorFlow.value = res.flow
twoFactorSecret.value = res.secret;
twoFactorFlow.value = res.flow;
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
const twoFactorIncorrect = ref(false)
const twoFactorCode = ref(null)
const backupCodes = ref([])
const twoFactorIncorrect = ref(false);
const twoFactorCode = ref(null);
const backupCodes = ref([]);
async function verifyTwoFactorCode() {
startLoading()
startLoading();
try {
const res = await useBaseFetch('auth/2fa', {
method: 'POST',
const res = await useBaseFetch("auth/2fa", {
method: "POST",
body: {
code: twoFactorCode.value ? twoFactorCode.value : '',
code: twoFactorCode.value ? twoFactorCode.value : "",
flow: twoFactorFlow.value,
},
})
});
backupCodes.value = res.backup_codes
twoFactorStep.value = 2
await useAuth(auth.value.token)
backupCodes.value = res.backup_codes;
twoFactorStep.value = 2;
await useAuth(auth.value.token);
} catch (err) {
twoFactorIncorrect.value = true
twoFactorIncorrect.value = true;
}
stopLoading()
stopLoading();
}
async function removeTwoFactor() {
startLoading()
startLoading();
try {
await useBaseFetch('auth/2fa', {
method: 'DELETE',
await useBaseFetch("auth/2fa", {
method: "DELETE",
body: {
code: twoFactorCode.value ? twoFactorCode.value.toString() : '',
code: twoFactorCode.value ? twoFactorCode.value.toString() : "",
},
})
manageTwoFactorModal.value.hide()
await useAuth(auth.value.token)
});
manageTwoFactorModal.value.hide();
await useAuth(auth.value.token);
} catch (err) {
twoFactorIncorrect.value = true
twoFactorIncorrect.value = true;
}
stopLoading()
stopLoading();
}
const authProviders = [
{
id: 'github',
display: 'GitHub',
id: "github",
display: "GitHub",
icon: GitHubIcon,
},
{
id: 'gitlab',
display: 'GitLab',
id: "gitlab",
display: "GitLab",
icon: GitLabIcon,
},
{
id: 'steam',
display: 'Steam',
id: "steam",
display: "Steam",
icon: SteamIcon,
},
{
id: 'discord',
display: 'Discord',
id: "discord",
display: "Discord",
icon: DiscordIcon,
},
{
id: 'microsoft',
display: 'Microsoft',
id: "microsoft",
display: "Microsoft",
icon: MicrosoftIcon,
},
{
id: 'google',
display: 'Google',
id: "google",
display: "Google",
icon: GoogleIcon,
},
]
];
async function deleteAccount() {
startLoading()
startLoading();
try {
await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'DELETE',
})
method: "DELETE",
});
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data.description,
type: 'error',
})
type: "error",
});
}
useCookie('auth-token').value = null
window.location.href = '/'
useCookie("auth-token").value = null;
window.location.href = "/";
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -134,13 +134,13 @@
class="btn btn-primary"
@click="
() => {
name = null
icon = null
scopesVal = 0
redirectUris = ['']
editingId = null
expires = null
$refs.appModal.show()
name = null;
icon = null;
scopesVal = 0;
redirectUris = [''];
editingId = null;
expires = null;
$refs.appModal.show();
}
"
>
@@ -189,8 +189,8 @@
setForm({
...app,
redirect_uris: app.redirect_uris.map((u) => u.uri) || [],
})
$refs.appModal.show()
});
$refs.appModal.show();
}
"
>
@@ -202,8 +202,8 @@
icon-only
@click="
() => {
editingId = app.id
$refs.modal_confirm.show()
editingId = app.id;
$refs.modal_confirm.show();
}
"
>
@@ -215,9 +215,9 @@
</div>
</template>
<script setup>
import { UploadIcon, PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from '@modrinth/assets'
import { CopyCode, ConfirmModal, Button, Checkbox, Avatar, FileInput } from '@modrinth/ui'
import Modal from '~/components/ui/Modal.vue'
import { UploadIcon, PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { CopyCode, ConfirmModal, Button, Checkbox, Avatar, FileInput } from "@modrinth/ui";
import Modal from "~/components/ui/Modal.vue";
import {
scopeList,
@@ -225,132 +225,132 @@ import {
toggleScope,
useScopes,
getScopeValue,
} from '~/composables/auth/scopes.ts'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
} from "~/composables/auth/scopes.ts";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: 'Applications - Modrinth',
})
title: "Applications - Modrinth",
});
const data = useNuxtApp()
const { scopesToLabels } = useScopes()
const data = useNuxtApp();
const { scopesToLabels } = useScopes();
const appModal = ref()
const appModal = ref();
// Any apps created in the current state will be stored here
// Users can copy Client Secrets and such before the page reloads
const createdApps = ref([])
const createdApps = ref([]);
const editingId = ref(null)
const name = ref(null)
const icon = ref(null)
const scopesVal = ref(BigInt(0))
const redirectUris = ref([''])
const url = ref(null)
const description = ref(null)
const editingId = ref(null);
const name = ref(null);
const icon = ref(null);
const scopesVal = ref(BigInt(0));
const redirectUris = ref([""]);
const url = ref(null);
const description = ref(null);
const loading = ref(false)
const loading = ref(false);
const auth = await useAuth()
const auth = await useAuth();
const { data: usersApps, refresh } = await useAsyncData(
'usersApps',
"usersApps",
() =>
useBaseFetch(`user/${auth.value.user.id}/oauth_apps`, {
apiVersion: 3,
}),
{
watch: [auth],
}
)
},
);
const setForm = (app) => {
if (app?.id) {
editingId.value = app.id
editingId.value = app.id;
} else {
editingId.value = null
editingId.value = null;
}
name.value = app?.name || ''
icon.value = app?.icon_url || ''
scopesVal.value = app?.max_scopes || BigInt(0)
url.value = app?.url || ''
description.value = app?.description || ''
name.value = app?.name || "";
icon.value = app?.icon_url || "";
scopesVal.value = app?.max_scopes || BigInt(0);
url.value = app?.url || "";
description.value = app?.description || "";
if (app?.redirect_uris) {
redirectUris.value = app.redirect_uris.map((uri) => uri?.uri || uri)
redirectUris.value = app.redirect_uris.map((uri) => uri?.uri || uri);
} else {
redirectUris.value = ['']
redirectUris.value = [""];
}
}
};
const canSubmit = computed(() => {
// Make sure name, scopes, and return uri are at least filled in
const filledIn =
name.value && name.value !== '' && name.value?.length > 2 && redirectUris.value.length > 0
name.value && name.value !== "" && name.value?.length > 2 && redirectUris.value.length > 0;
// Make sure the redirect uris are either one empty string or all filled in with valid urls
const oneValid = redirectUris.value.length === 1 && redirectUris.value[0] === ''
let allValid
const oneValid = redirectUris.value.length === 1 && redirectUris.value[0] === "";
let allValid;
try {
allValid = redirectUris.value.every((uri) => {
const url = new URL(uri)
return !!url
})
const url = new URL(uri);
return !!url;
});
} catch (err) {
allValid = false
allValid = false;
}
return filledIn && (oneValid || allValid)
})
return filledIn && (oneValid || allValid);
});
const clientCreatedInState = (id) => {
return createdApps.value.find((app) => app.id === id)
}
return createdApps.value.find((app) => app.id === id);
};
async function onImageSelection(files) {
if (!editingId.value) {
throw new Error('No editing id')
throw new Error("No editing id");
}
if (files.length > 0) {
const file = files[0]
const extFromType = file.type.split('/')[1]
const file = files[0];
const extFromType = file.type.split("/")[1];
await useBaseFetch('oauth/app/' + editingId.value + '/icon', {
method: 'PATCH',
await useBaseFetch("oauth/app/" + editingId.value + "/icon", {
method: "PATCH",
internal: true,
body: file,
query: {
ext: extFromType,
},
})
});
await refresh()
await refresh();
const app = usersApps.value.find((app) => app.id === editingId.value)
const app = usersApps.value.find((app) => app.id === editingId.value);
if (app) {
setForm(app)
setForm(app);
}
data.$notify({
group: 'main',
title: 'Icon updated',
text: 'Your application icon has been updated.',
type: 'success',
})
group: "main",
title: "Icon updated",
text: "Your application icon has been updated.",
type: "success",
});
}
}
async function createApp() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
const createdAppInfo = await useBaseFetch('oauth/app', {
method: 'POST',
const createdAppInfo = await useBaseFetch("oauth/app", {
method: "POST",
internal: true,
body: {
name: name.value,
@@ -358,38 +358,38 @@ async function createApp() {
max_scopes: Number(scopesVal.value), // JS is 52 bit for ints so we're good for now
redirect_uris: redirectUris.value,
},
})
});
createdApps.value.push(createdAppInfo)
createdApps.value.push(createdAppInfo);
setForm(null)
appModal.value.hide()
setForm(null);
appModal.value.hide();
await refresh()
await refresh();
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function editApp() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
if (!editingId.value) {
throw new Error('No editing id')
throw new Error("No editing id");
}
// check if there's any difference between the current app and the one in the state
const app = usersApps.value.find((app) => app.id === editingId.value)
const app = usersApps.value.find((app) => app.id === editingId.value);
if (!app) {
throw new Error('No app found')
throw new Error("No app found");
}
if (
@@ -400,74 +400,74 @@ async function editApp() {
app.url === url.value &&
app.description === description.value
) {
setForm(null)
editingId.value = null
appModal.value.hide()
throw new Error('No changes detected')
setForm(null);
editingId.value = null;
appModal.value.hide();
throw new Error("No changes detected");
}
const body = {
name: name.value,
max_scopes: Number(scopesVal.value), // JS is 52 bit for ints so we're good for now
redirect_uris: redirectUris.value,
}
};
if (url.value && url.value?.length > 0) {
body.url = url.value
body.url = url.value;
}
if (description.value && description.value?.length > 0) {
body.description = description.value
body.description = description.value;
}
if (icon.value && icon.value?.length > 0) {
body.icon_url = icon.value
body.icon_url = icon.value;
}
await useBaseFetch('oauth/app/' + editingId.value, {
method: 'PATCH',
await useBaseFetch("oauth/app/" + editingId.value, {
method: "PATCH",
internal: true,
body,
})
});
await refresh()
setForm(null)
editingId.value = null
await refresh();
setForm(null);
editingId.value = null;
appModal.value.hide()
appModal.value.hide();
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function removeApp() {
startLoading()
startLoading();
try {
if (!editingId.value) {
throw new Error('No editing id')
throw new Error("No editing id");
}
await useBaseFetch(`oauth/app/${editingId.value}`, {
internal: true,
method: 'DELETE',
})
await refresh()
editingId.value = null
method: "DELETE",
});
await refresh();
editingId.value = null;
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -75,8 +75,8 @@
icon-only
@click="
() => {
revokingId = authorization.app_id
$refs.modal_confirm.show()
revokingId = authorization.app_id;
$refs.modal_confirm.show();
}
"
>
@@ -88,88 +88,88 @@
</div>
</template>
<script setup>
import { Button, ConfirmModal, Avatar } from '@modrinth/ui'
import { TrashIcon, CheckIcon } from '@modrinth/assets'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import { useScopes } from '~/composables/auth/scopes.ts'
import { Button, ConfirmModal, Avatar } from "@modrinth/ui";
import { TrashIcon, CheckIcon } from "@modrinth/assets";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
import { useScopes } from "~/composables/auth/scopes.ts";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const { scopesToDefinitions } = useScopes()
const { scopesToDefinitions } = useScopes();
const revokingId = ref(null)
const revokingId = ref(null);
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: 'Authorizations - Modrinth',
})
title: "Authorizations - Modrinth",
});
const { data: usersApps, refresh } = await useAsyncData('userAuthorizations', () =>
const { data: usersApps, refresh } = await useAsyncData("userAuthorizations", () =>
useBaseFetch(`oauth/authorizations`, {
internal: true,
})
)
}),
);
const { data: appInformation } = await useAsyncData(
'appInfo',
"appInfo",
() =>
useBaseFetch('oauth/apps', {
useBaseFetch("oauth/apps", {
internal: true,
query: {
ids: usersApps.value.map((c) => c.app_id).join(','),
ids: usersApps.value.map((c) => c.app_id).join(","),
},
}),
{
watch: usersApps,
}
)
},
);
const { data: appCreatorsInformation } = await useAsyncData(
'appCreatorsInfo',
"appCreatorsInfo",
() =>
useBaseFetch('users', {
useBaseFetch("users", {
query: {
ids: JSON.stringify(appInformation.value.map((c) => c.created_by)),
},
}),
{
watch: appInformation,
}
)
},
);
const appInfoLookup = computed(() => {
return usersApps.value.map((app) => {
const info = appInformation.value.find((c) => c.id === app.app_id)
const owner = appCreatorsInformation.value.find((c) => c.id === info.created_by)
const info = appInformation.value.find((c) => c.id === app.app_id);
const owner = appCreatorsInformation.value.find((c) => c.id === info.created_by);
return {
...app,
app: info || null,
owner: owner || null,
}
})
})
};
});
});
async function revokeApp(id) {
try {
await useBaseFetch(`oauth/authorizations`, {
internal: true,
method: 'DELETE',
method: "DELETE",
query: {
client_id: id,
},
})
revokingId.value = null
await refresh()
});
revokingId.value = null;
await refresh();
} catch (err) {
data.$notify({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
}
</script>

View File

@@ -229,234 +229,234 @@
</template>
<script setup>
import { CodeIcon, RadioButtonIcon, RadioButtonChecked, SunIcon, MoonIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui'
import { formatProjectType } from '~/plugins/shorthands.js'
import MessageBanner from '~/components/ui/MessageBanner.vue'
import { DARK_THEMES } from '~/composables/theme.js'
import { CodeIcon, RadioButtonIcon, RadioButtonChecked, SunIcon, MoonIcon } from "@modrinth/assets";
import { Button } from "@modrinth/ui";
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',
})
title: "Display settings - Modrinth",
});
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const developerModeBanner = defineMessages({
description: {
id: 'settings.display.banner.developer-mode.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',
id: "settings.display.banner.developer-mode.button",
defaultMessage: "Deactivate developer mode",
},
})
});
const colorTheme = defineMessages({
title: {
id: 'settings.display.theme.title',
defaultMessage: 'Color theme',
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.',
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',
id: "settings.display.theme.system",
defaultMessage: "Sync with system",
},
light: {
id: 'settings.display.theme.light',
defaultMessage: 'Light',
id: "settings.display.theme.light",
defaultMessage: "Light",
},
dark: {
id: 'settings.display.theme.dark',
defaultMessage: 'Dark',
id: "settings.display.theme.dark",
defaultMessage: "Dark",
},
oled: {
id: 'settings.display.theme.oled',
defaultMessage: 'OLED',
id: "settings.display.theme.oled",
defaultMessage: "OLED",
},
retro: {
id: 'settings.display.theme.retro',
defaultMessage: 'Retro',
id: "settings.display.theme.retro",
defaultMessage: "Retro",
},
preferredLight: {
id: 'settings.display.theme.preferred-light-theme',
defaultMessage: 'Preferred light theme',
id: "settings.display.theme.preferred-light-theme",
defaultMessage: "Preferred light theme",
},
preferredDark: {
id: 'settings.display.theme.preferred-dark-theme',
defaultMessage: 'Preferred dark theme',
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',
id: "settings.display.project-list-layouts.title",
defaultMessage: "Project list layouts",
},
description: {
id: 'settings.display.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.',
"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',
id: "settings.display.project-list-layouts.mod",
defaultMessage: "Mods page",
},
plugin: {
id: 'settings.display.project-list-layouts.plugin',
defaultMessage: 'Plugins page',
id: "settings.display.project-list-layouts.plugin",
defaultMessage: "Plugins page",
},
datapack: {
id: 'settings.display.project-list-layouts.datapack',
defaultMessage: 'Data Packs page',
id: "settings.display.project-list-layouts.datapack",
defaultMessage: "Data Packs page",
},
shader: {
id: 'settings.display.project-list-layouts.shader',
defaultMessage: 'Shaders page',
id: "settings.display.project-list-layouts.shader",
defaultMessage: "Shaders page",
},
resourcepack: {
id: 'settings.display.project-list-layouts.resourcepack',
defaultMessage: 'Resource Packs page',
id: "settings.display.project-list-layouts.resourcepack",
defaultMessage: "Resource Packs page",
},
modpack: {
id: 'settings.display.project-list-layouts.modpack',
defaultMessage: 'Modpacks page',
id: "settings.display.project-list-layouts.modpack",
defaultMessage: "Modpacks page",
},
user: {
id: 'settings.display.project-list-layouts.user',
defaultMessage: 'User profile pages',
id: "settings.display.project-list-layouts.user",
defaultMessage: "User profile pages",
},
})
});
const toggleFeatures = defineMessages({
title: {
id: 'settings.display.flags.title',
defaultMessage: 'Toggle features',
id: "settings.display.flags.title",
defaultMessage: "Toggle features",
},
description: {
id: 'settings.display.flags.description',
defaultMessage: 'Enable or disable certain features on this device.',
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',
id: "settings.display.sidebar.advanced-rendering.title",
defaultMessage: "Advanced rendering",
},
advancedRenderingDescription: {
id: 'settings.display.sidebar.advanced-rendering.description',
id: "settings.display.sidebar.advanced-rendering.description",
defaultMessage:
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
"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',
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',
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.',
"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',
id: "settings.display.sidebar.hide-app-promos.title",
defaultMessage: "Hide Modrinth App promotions",
},
hideModrinthAppPromosDescription: {
id: 'settings.display.sidebar.hide-app-promos.description',
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',
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.',
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',
id: "settings.display.sidebar.right-aligned-project-sidebar.title",
defaultMessage: "Right-aligned project sidebar",
},
rightAlignedProjectSidebarDescription: {
id: 'settings.display.sidebar.right-aligned-project-sidebar.description',
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 flags = useFeatureFlags()
const tags = useTags()
const cosmetics = useCosmetics();
const flags = useFeatureFlags();
const tags = useTags();
const systemTheme = ref('light')
const systemTheme = ref("light");
const theme = useTheme()
const theme = useTheme();
const themeOptions = computed(() => {
const options = ['system', 'light', 'dark', 'oled']
if (flags.value.developerMode || theme.value.preference === 'retro') {
options.push('retro')
const options = ["system", "light", "dark", "oled"];
if (flags.value.developerMode || theme.value.preference === "retro") {
options.push("retro");
}
return options
})
return options;
});
onMounted(() => {
updateSystemTheme()
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (event) => {
setSystemTheme(event.matches)
})
})
updateSystemTheme();
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", (event) => {
setSystemTheme(event.matches);
});
});
function updateSystemTheme() {
const query = window.matchMedia('(prefers-color-scheme: light)')
setSystemTheme(query.matches)
const query = window.matchMedia("(prefers-color-scheme: light)");
setSystemTheme(query.matches);
}
function setSystemTheme(light) {
if (light) {
systemTheme.value = 'light'
systemTheme.value = "light";
} else {
systemTheme.value = cosmetics.value.preferredDarkTheme ?? 'dark'
systemTheme.value = cosmetics.value.preferredDarkTheme ?? "dark";
}
}
function updateColorTheme(value) {
if (DARK_THEMES.includes(value)) {
cosmetics.value.preferredDarkTheme = value
saveCosmetics()
updateSystemTheme()
cosmetics.value.preferredDarkTheme = value;
saveCosmetics();
updateSystemTheme();
}
updateTheme(value, true)
updateTheme(value, true);
}
function disableDeveloperMode() {
flags.value.developerMode = !flags.value.developerMode
saveFeatureFlags()
flags.value.developerMode = !flags.value.developerMode;
saveFeatureFlags();
addNotification({
group: 'main',
title: 'Developer mode deactivated',
text: 'Developer mode has been disabled',
type: 'success',
})
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) + 's',
display: 'the ' + formatProjectType(type.id).toLowerCase() + 's search page',
}
})
name: formatProjectType(type.id) + "s",
display: "the " + formatProjectType(type.id).toLowerCase() + "s search page",
};
});
types.push({
id: 'user',
name: 'User profiles',
display: 'user pages',
})
return types
})
id: "user",
name: "User profiles",
display: "user pages",
});
return types;
});
</script>
<style scoped lang="scss">
.preview-radio {
@@ -525,7 +525,7 @@ const listTypes = computed(() => {
margin: 0;
padding: 1rem;
display: grid;
grid-template: 'icon text1' 'icon text2';
grid-template: "icon text1" "icon text2";
grid-template-columns: auto 1fr;
gap: 0.5rem;
outline: 2px solid transparent;

View File

@@ -1,151 +1,151 @@
<script setup lang="ts">
import Fuse from 'fuse.js/dist/fuse.basic'
import RadioButtonIcon from '~/assets/images/utils/radio-button.svg?component'
import RadioButtonCheckedIcon from '~/assets/images/utils/radio-button-checked.svg?component'
import WarningIcon from '~/assets/images/utils/issues.svg?component'
import { isModifierKeyDown } from '~/helpers/events.ts'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import Fuse from "fuse.js/dist/fuse.basic";
import RadioButtonIcon from "~/assets/images/utils/radio-button.svg?component";
import RadioButtonCheckedIcon from "~/assets/images/utils/radio-button-checked.svg?component";
import WarningIcon from "~/assets/images/utils/issues.svg?component";
import { isModifierKeyDown } from "~/helpers/events.ts";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
const vintl = useVIntl()
const { formatMessage } = vintl
const vintl = useVIntl();
const { formatMessage } = vintl;
const messages = defineMessages({
languagesDescription: {
id: 'settings.language.description',
id: "settings.language.description",
defaultMessage:
'Choose your preferred language for the site. Translations are contributed by volunteers <crowdin-link>on Crowdin</crowdin-link>.',
"Choose your preferred language for the site. Translations are contributed by volunteers <crowdin-link>on Crowdin</crowdin-link>.",
},
automaticLocale: {
id: 'settings.language.languages.automatic',
defaultMessage: 'Sync with the system language',
id: "settings.language.languages.automatic",
defaultMessage: "Sync with the system language",
},
noResults: {
id: 'settings.language.languages.search.no-results',
defaultMessage: 'No languages match your search.',
id: "settings.language.languages.search.no-results",
defaultMessage: "No languages match your search.",
},
searchFieldDescription: {
id: 'settings.language.languages.search-field.description',
defaultMessage: 'Submit to focus the first search result',
id: "settings.language.languages.search-field.description",
defaultMessage: "Submit to focus the first search result",
},
searchFieldPlaceholder: {
id: 'settings.language.languages.search-field.placeholder',
defaultMessage: 'Search for a language...',
id: "settings.language.languages.search-field.placeholder",
defaultMessage: "Search for a language...",
},
searchResultsAnnouncement: {
id: 'settings.language.languages.search-results-announcement',
id: "settings.language.languages.search-results-announcement",
defaultMessage:
'{matches, plural, =0 {No languages match} one {# language matches} other {# languages match}} your search.',
"{matches, plural, =0 {No languages match} one {# language matches} other {# languages match}} your search.",
},
loadFailed: {
id: 'settings.language.languages.load-failed',
defaultMessage: 'Cannot load this language. Try again in a bit.',
id: "settings.language.languages.load-failed",
defaultMessage: "Cannot load this language. Try again in a bit.",
},
languageLabelApplying: {
id: 'settings.language.languages.language-label-applying',
defaultMessage: '{label}. Applying...',
id: "settings.language.languages.language-label-applying",
defaultMessage: "{label}. Applying...",
},
languageLabelError: {
id: 'settings.language.languages.language-label-error',
defaultMessage: '{label}. Error',
id: "settings.language.languages.language-label-error",
defaultMessage: "{label}. Error",
},
})
});
const categoryNames = defineMessages({
auto: {
id: 'settings.language.categories.auto',
defaultMessage: 'Automatic',
id: "settings.language.categories.auto",
defaultMessage: "Automatic",
},
default: {
id: 'settings.language.categories.default',
defaultMessage: 'Standard languages',
id: "settings.language.categories.default",
defaultMessage: "Standard languages",
},
fun: {
id: 'settings.language.categories.fun',
defaultMessage: 'Fun languages',
id: "settings.language.categories.fun",
defaultMessage: "Fun languages",
},
experimental: {
id: 'settings.language.categories.experimental',
defaultMessage: 'Experimental languages',
id: "settings.language.categories.experimental",
defaultMessage: "Experimental languages",
},
searchResult: {
id: 'settings.language.categories.search-result',
defaultMessage: 'Search results',
id: "settings.language.categories.search-result",
defaultMessage: "Search results",
},
})
});
type Category = keyof typeof categoryNames
type Category = keyof typeof categoryNames;
const categoryOrder: Category[] = ['auto', 'default', 'fun', 'experimental']
const categoryOrder: Category[] = ["auto", "default", "fun", "experimental"];
function normalizeCategoryName(name?: string): keyof typeof categoryNames {
switch (name) {
case 'auto':
case 'fun':
case 'experimental':
return name
case "auto":
case "fun":
case "experimental":
return name;
default:
return 'default'
return "default";
}
}
type LocaleBase = {
category: Category
tag: string
searchTerms?: string[]
}
category: Category;
tag: string;
searchTerms?: string[];
};
type AutomaticLocale = LocaleBase & {
auto: true
}
auto: true;
};
type CommonLocale = LocaleBase & {
auto?: never
displayName: string
defaultName: string
translatedName: string
}
auto?: never;
displayName: string;
defaultName: string;
translatedName: string;
};
type Locale = AutomaticLocale | CommonLocale
type Locale = AutomaticLocale | CommonLocale;
const $defaultNames = useDisplayNames(() => vintl.defaultLocale)
const $defaultNames = useDisplayNames(() => vintl.defaultLocale);
const $translatedNames = useDisplayNames(() => vintl.locale)
const $translatedNames = useDisplayNames(() => vintl.locale);
const $locales = computed(() => {
const locales: Locale[] = []
const locales: Locale[] = [];
locales.push({
auto: true,
tag: 'auto',
category: 'auto',
tag: "auto",
category: "auto",
searchTerms: [
'automatic',
'Sync with the system language',
"automatic",
"Sync with the system language",
formatMessage(messages.automaticLocale),
],
})
});
for (const locale of vintl.availableLocales) {
let displayName = locale.meta?.displayName
let displayName = locale.meta?.displayName;
if (displayName == null) {
displayName = createDisplayNames(locale.tag).of(locale.tag) ?? locale.tag
displayName = createDisplayNames(locale.tag).of(locale.tag) ?? locale.tag;
}
let defaultName = vintl.defaultResources['languages.json']?.[locale.tag]
let defaultName = vintl.defaultResources["languages.json"]?.[locale.tag];
if (defaultName == null) {
defaultName = $defaultNames.value.of(locale.tag) ?? locale.tag
defaultName = $defaultNames.value.of(locale.tag) ?? locale.tag;
}
let translatedName = vintl.resources['languages.json']?.[locale.tag]
let translatedName = vintl.resources["languages.json"]?.[locale.tag];
if (translatedName == null) {
translatedName = $translatedNames.value.of(locale.tag) ?? locale.tag
translatedName = $translatedNames.value.of(locale.tag) ?? locale.tag;
}
let searchTerms = locale.meta?.searchTerms
if (searchTerms === '-') searchTerms = undefined
let searchTerms = locale.meta?.searchTerms;
if (searchTerms === "-") searchTerms = undefined;
locales.push({
tag: locale.tag,
@@ -153,132 +153,132 @@ const $locales = computed(() => {
displayName,
defaultName,
translatedName,
searchTerms: searchTerms?.split('\n'),
})
searchTerms: searchTerms?.split("\n"),
});
}
return locales
})
return locales;
});
const $query = ref('')
const $query = ref("");
const isQueryEmpty = () => $query.value.trim().length === 0
const isQueryEmpty = () => $query.value.trim().length === 0;
const fuse = new Fuse<Locale>([], {
keys: ['tag', 'displayName', 'translatedName', 'englishName', 'searchTerms'],
keys: ["tag", "displayName", "translatedName", "englishName", "searchTerms"],
threshold: 0.4,
distance: 100,
})
});
watchSyncEffect(() => fuse.setCollection($locales.value))
watchSyncEffect(() => fuse.setCollection($locales.value));
const $categories = computed(() => {
const categories = new Map<Category, Locale[]>()
const categories = new Map<Category, Locale[]>();
for (const category of categoryOrder) categories.set(category, [])
for (const category of categoryOrder) categories.set(category, []);
for (const locale of $locales.value) {
let categoryLocales = categories.get(locale.category)
let categoryLocales = categories.get(locale.category);
if (categoryLocales == null) {
categoryLocales = []
categories.set(locale.category, categoryLocales)
categoryLocales = [];
categories.set(locale.category, categoryLocales);
}
categoryLocales.push(locale)
categoryLocales.push(locale);
}
for (const categoryKey of [...categories.keys()]) {
if (categories.get(categoryKey)?.length === 0) {
categories.delete(categoryKey)
categories.delete(categoryKey);
}
}
return categories
})
return categories;
});
const $searchResults = computed(() => {
return new Map<Category, Locale[]>([
['searchResult', isQueryEmpty() ? [] : fuse.search($query.value).map(({ item }) => item)],
])
})
["searchResult", isQueryEmpty() ? [] : fuse.search($query.value).map(({ item }) => item)],
]);
});
const $displayCategories = computed(() =>
isQueryEmpty() ? $categories.value : $searchResults.value
)
isQueryEmpty() ? $categories.value : $searchResults.value,
);
const $changingTo = ref<string | undefined>()
const $changingTo = ref<string | undefined>();
const isChanging = () => $changingTo.value != null
const isChanging = () => $changingTo.value != null;
const $failedLocale = ref<string>()
const $failedLocale = ref<string>();
const $activeLocale = computed(() => {
if ($changingTo.value != null) return $changingTo.value
return vintl.automatic ? 'auto' : vintl.locale
})
if ($changingTo.value != null) return $changingTo.value;
return vintl.automatic ? "auto" : vintl.locale;
});
async function changeLocale(value: string) {
if ($activeLocale.value === value) return
if ($activeLocale.value === value) return;
$changingTo.value = value
$changingTo.value = value;
try {
await vintl.changeLocale(value)
$failedLocale.value = undefined
await vintl.changeLocale(value);
$failedLocale.value = undefined;
} catch (err) {
$failedLocale.value = value
$failedLocale.value = value;
} finally {
$changingTo.value = undefined
$changingTo.value = undefined;
}
}
const $languagesList = ref<HTMLDivElement | undefined>()
const $languagesList = ref<HTMLDivElement | undefined>();
function onSearchKeydown(e: KeyboardEvent) {
if (e.key !== 'Enter' || isModifierKeyDown(e)) return
if (e.key !== "Enter" || isModifierKeyDown(e)) return;
const focusableTarget = $languagesList.value?.querySelector(
'input, [tabindex]:not([tabindex="-1"])'
) as HTMLElement | undefined
'input, [tabindex]:not([tabindex="-1"])',
) as HTMLElement | undefined;
focusableTarget?.focus()
focusableTarget?.focus();
}
function onItemKeydown(e: KeyboardEvent, locale: Locale) {
switch (e.key) {
case 'Enter':
case ' ':
break
case "Enter":
case " ":
break;
default:
return
return;
}
if (isModifierKeyDown(e) || isChanging()) return
if (isModifierKeyDown(e) || isChanging()) return;
changeLocale(locale.tag)
changeLocale(locale.tag);
}
function onItemClick(e: MouseEvent, locale: Locale) {
if (isModifierKeyDown(e) || isChanging()) return
if (isModifierKeyDown(e) || isChanging()) return;
changeLocale(locale.tag)
changeLocale(locale.tag);
}
function getItemLabel(locale: Locale) {
const label = locale.auto
? formatMessage(messages.automaticLocale)
: `${locale.translatedName}. ${locale.displayName}`
: `${locale.translatedName}. ${locale.displayName}`;
if ($changingTo.value === locale.tag) {
return formatMessage(messages.languageLabelApplying, { label })
return formatMessage(messages.languageLabelApplying, { label });
}
if ($failedLocale.value === locale.tag) {
return formatMessage(messages.languageLabelError, { label })
return formatMessage(messages.languageLabelError, { label });
}
return label
return label;
}
</script>
@@ -317,9 +317,9 @@ function getItemLabel(locale: Locale) {
<div id="language-search-results-announcements" class="visually-hidden" aria-live="polite">
{{
isQueryEmpty()
? ''
? ""
: formatMessage(messages.searchResultsAnnouncement, {
matches: $searchResults.get('searchResult')?.length ?? 0,
matches: $searchResults.get("searchResult")?.length ?? 0,
})
}}
</div>
@@ -404,7 +404,7 @@ function getItemLabel(locale: Locale) {
position: relative;
overflow: hidden;
&:not([aria-disabled='true']):hover {
&:not([aria-disabled="true"]):hover {
border-color: var(--color-button-bg-hover);
}
@@ -422,7 +422,7 @@ function getItemLabel(locale: Locale) {
}
&.pending::after {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@@ -465,7 +465,7 @@ function getItemLabel(locale: Locale) {
}
}
&[aria-disabled='true']:not(.pending) {
&[aria-disabled="true"]:not(.pending) {
opacity: 0.8;
pointer-events: none;
cursor: default;

View File

@@ -80,11 +80,11 @@
class="btn btn-primary"
@click="
() => {
name = null
scopesVal = 0
expires = null
editPatIndex = null
$refs.patModal.show()
name = null;
scopesVal = 0;
expires = null;
editPatIndex = null;
$refs.patModal.show();
}
"
>
@@ -176,11 +176,11 @@
class="iconified-button raised-button"
@click="
() => {
editPatIndex = index
name = pat.name
scopesVal = pat.scopes
expires = $dayjs(pat.expires).format('YYYY-MM-DD')
$refs.patModal.show()
editPatIndex = index;
name = pat.name;
scopesVal = pat.scopes;
expires = $dayjs(pat.expires).format('YYYY-MM-DD');
$refs.patModal.show();
}
"
>
@@ -190,8 +190,8 @@
class="iconified-button raised-button"
@click="
() => {
deletePatIndex = pat.id
$refs.modal_confirm.show()
deletePatIndex = pat.id;
$refs.modal_confirm.show();
}
"
>
@@ -202,199 +202,199 @@
</div>
</template>
<script setup>
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from '@modrinth/assets'
import { Checkbox, ConfirmModal } from '@modrinth/ui'
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { Checkbox, ConfirmModal } from "@modrinth/ui";
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import { commonSettingsMessages } from "~/utils/common-messages.ts";
import {
hasScope,
scopeList,
toggleScope,
useScopes,
getScopeValue,
} from '~/composables/auth/scopes.ts'
} from "~/composables/auth/scopes.ts";
import CopyCode from '~/components/ui/CopyCode.vue'
import Modal from '~/components/ui/Modal.vue'
import CopyCode from "~/components/ui/CopyCode.vue";
import Modal from "~/components/ui/Modal.vue";
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime()
const formatRelativeTime = useRelativeTime();
const createModalMessages = defineMessages({
createTitle: {
id: 'settings.pats.modal.create.title',
defaultMessage: 'Create personal access token',
id: "settings.pats.modal.create.title",
defaultMessage: "Create personal access token",
},
editTitle: {
id: 'settings.pats.modal.edit.title',
defaultMessage: 'Edit personal access token',
id: "settings.pats.modal.edit.title",
defaultMessage: "Edit personal access token",
},
nameLabel: {
id: 'settings.pats.modal.create.name.label',
defaultMessage: 'Name',
id: "settings.pats.modal.create.name.label",
defaultMessage: "Name",
},
namePlaceholder: {
id: 'settings.pats.modal.create.name.placeholder',
id: "settings.pats.modal.create.name.placeholder",
defaultMessage: "Enter the PAT's name...",
},
expiresLabel: {
id: 'settings.pats.modal.create.expires.label',
defaultMessage: 'Expires',
id: "settings.pats.modal.create.expires.label",
defaultMessage: "Expires",
},
action: {
id: 'settings.pats.modal.create.action',
defaultMessage: 'Create PAT',
id: "settings.pats.modal.create.action",
defaultMessage: "Create PAT",
},
})
});
const deleteModalMessages = defineMessages({
title: {
id: 'settings.pats.modal.delete.title',
defaultMessage: 'Are you sure you want to delete this token?',
id: "settings.pats.modal.delete.title",
defaultMessage: "Are you sure you want to delete this token?",
},
description: {
id: 'settings.pats.modal.delete.description',
defaultMessage: 'This will remove this token forever (like really forever).',
id: "settings.pats.modal.delete.description",
defaultMessage: "This will remove this token forever (like really forever).",
},
action: {
id: 'settings.pats.modal.delete.action',
defaultMessage: 'Delete this token',
id: "settings.pats.modal.delete.action",
defaultMessage: "Delete this token",
},
})
});
const messages = defineMessages({
description: {
id: 'settings.pats.description',
id: "settings.pats.description",
defaultMessage:
"PATs can be used to access Modrinth's API. For more information, see <doc-link>Modrinth's API documentation</doc-link>. They can be created and revoked at any time.",
},
create: {
id: 'settings.pats.action.create',
defaultMessage: 'Create a PAT',
id: "settings.pats.action.create",
defaultMessage: "Create a PAT",
},
})
});
const tokenMessages = defineMessages({
edit: {
id: 'settings.pats.token.action.edit',
defaultMessage: 'Edit token',
id: "settings.pats.token.action.edit",
defaultMessage: "Edit token",
},
revoke: {
id: 'settings.pats.token.action.revoke',
defaultMessage: 'Revoke token',
id: "settings.pats.token.action.revoke",
defaultMessage: "Revoke token",
},
lastUsed: {
id: 'settings.pats.token.last-used',
defaultMessage: 'Last used {ago}',
id: "settings.pats.token.last-used",
defaultMessage: "Last used {ago}",
},
neverUsed: {
id: 'settings.pats.token.never-used',
defaultMessage: 'Never used',
id: "settings.pats.token.never-used",
defaultMessage: "Never used",
},
expiresIn: {
id: 'settings.pats.token.expires-in',
defaultMessage: 'Expires {inTime}',
id: "settings.pats.token.expires-in",
defaultMessage: "Expires {inTime}",
},
expiredAgo: {
id: 'settings.pats.token.expired-ago',
defaultMessage: 'Expired {ago}',
id: "settings.pats.token.expired-ago",
defaultMessage: "Expired {ago}",
},
})
});
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
useHead({
title: `${formatMessage(commonSettingsMessages.pats)} - Modrinth`,
})
});
const data = useNuxtApp()
const { scopesToLabels } = useScopes()
const patModal = ref()
const data = useNuxtApp();
const { scopesToLabels } = useScopes();
const patModal = ref();
const editPatIndex = ref(null)
const editPatIndex = ref(null);
const name = ref(null)
const scopesVal = ref(BigInt(0))
const expires = ref(null)
const name = ref(null);
const scopesVal = ref(BigInt(0));
const expires = ref(null);
const deletePatIndex = ref(null)
const deletePatIndex = ref(null);
const loading = ref(false)
const loading = ref(false);
const { data: pats, refresh } = await useAsyncData('pat', () => useBaseFetch('pat'))
const { data: pats, refresh } = await useAsyncData("pat", () => useBaseFetch("pat"));
async function createPat() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
const res = await useBaseFetch('pat', {
method: 'POST',
const res = await useBaseFetch("pat", {
method: "POST",
body: {
name: name.value,
scopes: Number(scopesVal.value),
expires: data.$dayjs(expires.value).toISOString(),
},
})
pats.value.push(res)
patModal.value.hide()
});
pats.value.push(res);
patModal.value.hide();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function editPat() {
startLoading()
loading.value = true
startLoading();
loading.value = true;
try {
await useBaseFetch(`pat/${pats.value[editPatIndex.value].id}`, {
method: 'PATCH',
method: "PATCH",
body: {
name: name.value,
scopes: Number(scopesVal.value),
expires: data.$dayjs(expires.value).toISOString(),
},
})
await refresh()
patModal.value.hide()
});
await refresh();
patModal.value.hide();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
loading.value = false
stopLoading()
loading.value = false;
stopLoading();
}
async function removePat(id) {
startLoading()
startLoading();
try {
pats.value = pats.value.filter((x) => x.id !== id)
pats.value = pats.value.filter((x) => x.id !== id);
await useBaseFetch(`pat/${id}`, {
method: 'DELETE',
})
await refresh()
method: "DELETE",
});
await refresh();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -36,8 +36,8 @@
v-if="previewImage"
:action="
() => {
icon = null
previewImage = null
icon = null;
previewImage = null;
}
"
>
@@ -86,137 +86,137 @@
</template>
<script setup>
import { UserIcon, SaveIcon, UploadIcon, UndoIcon, XIcon } from '@modrinth/assets'
import { Avatar, FileInput, Button } from '@modrinth/ui'
import { commonMessages } from '~/utils/common-messages.ts'
import { UserIcon, SaveIcon, UploadIcon, UndoIcon, XIcon } from "@modrinth/assets";
import { Avatar, FileInput, Button } from "@modrinth/ui";
import { commonMessages } from "~/utils/common-messages.ts";
useHead({
title: 'Profile settings - Modrinth',
})
title: "Profile settings - Modrinth",
});
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
const { formatMessage } = useVIntl()
const { formatMessage } = useVIntl();
const messages = defineMessages({
title: {
id: 'settings.profile.profile-info',
defaultMessage: 'Profile information',
id: "settings.profile.profile-info",
defaultMessage: "Profile information",
},
description: {
id: 'settings.profile.description',
id: "settings.profile.description",
defaultMessage:
'Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>.',
"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',
id: "settings.profile.profile-picture.title",
defaultMessage: "Profile picture",
},
profilePictureReset: {
id: 'settings.profile.profile-picture.reset',
defaultMessage: 'Reset',
id: "settings.profile.profile-picture.reset",
defaultMessage: "Reset",
},
usernameTitle: {
id: 'settings.profile.username.title',
defaultMessage: 'Username',
id: "settings.profile.username.title",
defaultMessage: "Username",
},
usernameDescription: {
id: 'settings.profile.username.description',
defaultMessage: 'A unique case-insensitive name to identify your profile.',
id: "settings.profile.username.description",
defaultMessage: "A unique case-insensitive name to identify your profile.",
},
bioTitle: {
id: 'settings.profile.bio.title',
defaultMessage: 'Bio',
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.',
id: "settings.profile.bio.description",
defaultMessage: "A short description to tell everyone a little bit about you.",
},
})
});
const auth = await useAuth()
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 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
)
previewImage.value,
);
function showPreviewImage(files) {
const reader = new FileReader()
icon.value = files[0]
reader.readAsDataURL(icon.value)
const reader = new FileReader();
icon.value = files[0];
reader.readAsDataURL(icon.value);
reader.onload = (event) => {
previewImage.value = event.target.result
}
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
icon.value = null;
previewImage.value = null;
username.value = auth.value.user.username;
bio.value = auth.value.user.bio;
}
async function saveChanges() {
startLoading()
startLoading();
try {
if (icon.value) {
await useBaseFetch(
`user/${auth.value.user.id}/icon?ext=${
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
icon.value.type.split("/")[icon.value.type.split("/").length - 1]
}`,
{
method: 'PATCH',
method: "PATCH",
body: icon.value,
}
)
icon.value = null
previewImage.value = null
},
);
icon.value = null;
previewImage.value = null;
}
const body = {}
const body = {};
if (auth.value.user.username !== username.value) {
body.username = username.value
body.username = username.value;
}
if (auth.value.user.bio !== bio.value) {
body.bio = bio.value
body.bio = bio.value;
}
await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'PATCH',
method: "PATCH",
body,
})
await useAuth(auth.value.token)
avatarUrl.value = auth.value.user.avatar_url
saved.value = true
});
await useAuth(auth.value.token);
avatarUrl.value = auth.value.user.avatar_url;
saved.value = true;
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
group: "main",
title: "An error occurred",
text: err
? err.data
? err.data.description
? err.data.description
: err.data
: err
: 'aaaaahhh',
type: 'error',
})
: "aaaaahhh",
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>

View File

@@ -56,74 +56,74 @@
</div>
</template>
<script setup>
import { XIcon } from '@modrinth/assets'
import { commonSettingsMessages } from '~/utils/common-messages.ts'
import { XIcon } from "@modrinth/assets";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
definePageMeta({
middleware: 'auth',
})
middleware: "auth",
});
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime();
const messages = defineMessages({
currentSessionLabel: {
id: 'settings.sessions.current-session',
defaultMessage: 'Current session',
id: "settings.sessions.current-session",
defaultMessage: "Current session",
},
revokeSessionButton: {
id: 'settings.sessions.action.revoke-session',
defaultMessage: 'Revoke session',
id: "settings.sessions.action.revoke-session",
defaultMessage: "Revoke session",
},
createdAgoLabel: {
id: 'settings.sessions.created-ago',
defaultMessage: 'Created {ago}',
id: "settings.sessions.created-ago",
defaultMessage: "Created {ago}",
},
sessionsDescription: {
id: 'settings.sessions.description',
id: "settings.sessions.description",
defaultMessage:
"Here are all the devices that are currently logged in with your Modrinth account. You can log out of each one individually.\n\nIf you see an entry you don't recognize, log out of that device and change your Modrinth account password immediately.",
},
lastAccessedAgoLabel: {
id: 'settings.sessions.last-accessed-ago',
defaultMessage: 'Last accessed {ago}',
id: "settings.sessions.last-accessed-ago",
defaultMessage: "Last accessed {ago}",
},
unknownOsLabel: {
id: 'settings.sessions.unknown-os',
defaultMessage: 'Unknown OS',
id: "settings.sessions.unknown-os",
defaultMessage: "Unknown OS",
},
unknownPlatformLabel: {
id: 'settings.sessions.unknown-platform',
defaultMessage: 'Unknown platform',
id: "settings.sessions.unknown-platform",
defaultMessage: "Unknown platform",
},
})
});
useHead({
title: () => `${formatMessage(commonSettingsMessages.sessions)} - Modrinth`,
})
});
const data = useNuxtApp()
const { data: sessions, refresh } = await useAsyncData('session/list', () =>
useBaseFetch('session/list')
)
const data = useNuxtApp();
const { data: sessions, refresh } = await useAsyncData("session/list", () =>
useBaseFetch("session/list"),
);
async function revokeSession(id) {
startLoading()
startLoading();
try {
sessions.value = sessions.value.filter((x) => x.id !== id)
sessions.value = sessions.value.filter((x) => x.id !== id);
await useBaseFetch(`session/${id}`, {
method: 'DELETE',
})
await refresh()
method: "DELETE",
});
await refresh();
} catch (err) {
data.$notify({
group: 'main',
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data.description,
type: 'error',
})
type: "error",
});
}
stopLoading()
stopLoading();
}
</script>
<style lang="scss" scoped>