You've already forked AstralRinth
forked from didirus/AstralRinth
Add TailwindCSS (#1252)
* Setup TailwindCSS * Fully setup configuration * Refactor some tailwind variables
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user