Compare commits

..
Author SHA1 Message Date
coolbot100s 4b73aee56a fix method typo 2025-08-05 12:59:06 -07:00
coolbot100s b4339350e0 Merge branch 'main' into cal/dev-124-project-validation 2025-08-05 12:56:57 -07:00
coolbot100s de11629402 lint fix 2025-08-05 12:48:52 -07:00
coolbot100s 29e4c6d18a correct vocabulary for resolutions tags warning and sort tags list in resolution tags nag 2025-08-05 12:46:42 -07:00
coolbot100s 474e482474 fix tags warning, and link shorteners and misused discord warnings to link settings page, reword some warnings. 2025-08-05 12:34:02 -07:00
fishstizandIMB11 e31197f649 feat(app): pass selected version to incompatibility warning modal (#4115)
Co-authored-by: IMB11 <hendersoncal117@gmail.com>
2025-08-05 11:10:02 +00:00
Calum H. ca48107e7e fix: formatjs 2025-08-05 11:24:14 +01:00
coolbot100s 1f4b43cf0d fix locale 2025-08-04 22:25:17 -07:00
coolbot100s c850a3438c Merge branch 'cal/dev-124-project-validation' of https://github.com/modrinth/code into cal/dev-124-project-validation 2025-08-04 15:01:04 -07:00
coolbot100s 9c6c368408 updates to tags checking and rest of the nag titles 2025-08-04 15:00:58 -07:00
IMB11 87dcb04c43 Merge branch 'main' into cal/dev-124-project-validation 2025-08-04 21:56:12 +01:00
IMB11 8c0afd1183 feat: update readme 2025-08-04 18:41:14 +01:00
coolbot100s 9db2fd33c6 Merge branch 'cal/dev-124-project-validation' of https://github.com/modrinth/code into cal/dev-124-project-validation 2025-08-04 10:36:55 -07:00
coolbot100s 63787eebb7 Tweak core and description nag titles, change image accessability nag logic. 2025-08-04 10:36:51 -07:00
IMB11 02bbac00a8 feat: key + sort nags by type 2025-08-04 18:24:13 +01:00
coolbot100s f15ceafd5f update links.ts 2025-08-02 18:03:47 -07:00
IMB11 56699fc9b5 fix: lint issues 2025-08-02 12:48:25 +01:00
IMB11 c470eea9ac Merge branch 'main' into cal/dev-124-project-validation 2025-08-02 12:41:49 +01:00
IMB11 88f653384e Merge branch 'main' into cal/dev-124-project-validation 2025-08-01 13:08:57 +01:00
IMB11 f606b20109 Merge branch 'main' into cal/dev-124-project-validation
Signed-off-by: IMB11 <hendersoncal117@gmail.com>
2025-07-31 13:45:17 +01:00
coolbot100s 26c81e3803 Correct plugin project type checking 2025-07-30 00:11:54 -07:00
coolbot100s f720438bdb refactor link checking helper functions, prevent misuse of dsc links, prevent link shortener usage, check if source required licensed projects have additional files, bump this check back to required. 2025-07-29 17:00:26 -07:00
coolbot100s 2806d20e17 bump source publication nag to warn until additional files can be checked. 2025-07-29 14:26:48 -07:00
coolbot100s b5f44ac5cd Merge branch 'cal/dev-124-project-validation' of https://github.com/modrinth/code into cal/dev-124-project-validation 2025-07-29 13:30:25 -07:00
IMB11 ad1fed91cf fix: description nag 2025-07-29 13:23:16 +01:00
coolbot100s ba0b09d9e3 Tweak links nags adding project type checking for source publication check, make description nag tonally consistent. 2025-07-29 00:44:59 -07:00
coolbot100s 9dbc9607f7 Description nag rephrasing and tweaks 2025-07-29 00:09:00 -07:00
coolbot100s f66bafc06b Remove redundant sentanceEnders check to reduce false positive. 2025-07-28 21:04:55 -07:00
coolbot100s 1d34a5989e Modify character limit numbers 2025-07-28 19:11:21 -07:00
coolbot100s c594e32bc7 Rephrase a few core nags 2025-07-28 19:02:37 -07:00
IMB11 dc258de3c2 fix: issues 2025-07-28 17:06:57 +01:00
IMB11 54cfe29f7d fix: summary char min 2025-07-28 16:55:47 +01:00
IMB11 5c487795c4 refactor: inline i18n 2025-07-28 16:51:31 +01:00
IMB11 b87fb1de00 fix: locale issues 2025-07-28 16:41:30 +01:00
IMB11 fe3d360215 Merge branch 'main' into cal/dev-124-project-validation
Signed-off-by: IMB11 <hendersoncal117@gmail.com>
2025-07-28 16:33:03 +01:00
IMB11 89351b4be4 Merge branch 'main' into cal/dev-124-project-validation 2025-07-21 18:35:59 +01:00
Prospector 1261c7e7c9 Merge branch 'main' into cal/dev-124-project-validation 2025-07-16 15:22:38 -07:00
IMB11 252b7bf965 Merge branch 'main' into cal/dev-124-project-validation 2025-07-16 20:54:21 +01:00
IMB11 28c8b59820 fix: minecraft title clause update 2025-07-16 20:52:01 +01:00
Calum 2f1a31a9e4 feat: impl intl 2025-07-14 13:34:01 +01:00
Calum 35636f7c00 feat: start on i18nifying nags 2025-07-14 13:19:56 +01:00
Calum bddf73119d fix: issues 2025-07-13 15:12:34 +01:00
Calum 24ca268aba Merge branch 'cal/dev-124-project-validation' of https://github.com/modrinth/code into cal/dev-124-project-validation 2025-07-13 15:10:07 +01:00
Calum 4934fb1e0d fix: lint 2025-07-13 15:09:56 +01:00
IMB11 4a7e8e98b0 Merge branch 'main' into cal/dev-124-project-validation 2025-07-13 15:08:15 +01:00
Calum 2f1627c000 fix: lint issues 2025-07-13 15:06:29 +01:00
Calum debcb57f47 feat: tags validations 2025-07-13 15:01:43 +01:00
Calum 27caf336cc fix: links page + add more validations 2025-07-13 14:34:56 +01:00
Calum 729d584757 feat: start work implementing validation checks using new nag system 2025-07-12 22:02:48 +01:00
Calum 59ad8eb426 feat: re add submitting/re-submit nags 2025-07-11 21:12:56 +01:00
Calum d33b06ea55 feat: continue work 2025-07-11 20:07:26 +01:00
Calum b2978096fd fix: shouldShow issues 2025-07-11 17:21:45 +01:00
Calum bcfceecf1b feat: start on frontend impl 2025-07-11 17:21:43 +01:00
Calum b98c1fe7b8 feat: set up typed nag (validators) system 2025-07-11 17:21:37 +01:00
153 changed files with 4037 additions and 2631 deletions

No files matched your search

+1 -2
View File
@@ -4,7 +4,6 @@
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"editor.detectIndentation": true, "editor.detectIndentation": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit"
"source.organizeImports": "always",
} }
} }
+52 -52
View File
@@ -1,35 +1,6 @@
<script setup> <script setup>
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component' import { computed, onMounted, onUnmounted, ref, watch, provide } from 'vue'
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue' import { RouterView, useRoute, useRouter } from 'vue-router'
import AccountsCard from '@/components/ui/AccountsCard.vue'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { get_user } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { useFetch } from '@/helpers/fetch.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
import { get } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
import { useError } from '@/store/error.js'
import { useInstall } from '@/store/install.js'
import { useLoading, useTheming } from '@/store/state'
import { import {
ArrowBigUpDashIcon, ArrowBigUpDashIcon,
ChangeSkinIcon, ChangeSkinIcon,
@@ -42,44 +13,68 @@ import {
LogOutIcon, LogOutIcon,
MaximizeIcon, MaximizeIcon,
MinimizeIcon, MinimizeIcon,
NewspaperIcon,
PlusIcon, PlusIcon,
RestoreIcon, RestoreIcon,
RightArrowIcon, RightArrowIcon,
SettingsIcon, SettingsIcon,
WorldIcon, WorldIcon,
XIcon, XIcon,
NewspaperIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { import {
Avatar, Avatar,
Button, Button,
ButtonStyled, ButtonStyled,
NewsArticleCard, Notifications,
NotificationPanel,
OverflowMenu, OverflowMenu,
provideNotificationManager, NewsArticleCard,
} from '@modrinth/ui' } from '@modrinth/ui'
import { renderString } from '@modrinth/utils' import { useLoading, useTheming } from '@/store/state'
import { getVersion } from '@tauri-apps/api/app' import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
import { invoke } from '@tauri-apps/api/core' import AccountsCard from '@/components/ui/AccountsCard.vue'
import { getCurrentWindow } from '@tauri-apps/api/window' import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { openUrl } from '@tauri-apps/plugin-opener' import { get } from '@/helpers/settings.ts'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { type } from '@tauri-apps/plugin-os' import { type } from '@tauri-apps/plugin-os'
import { check } from '@tauri-apps/plugin-updater' import { getOS, isDev, restartApp } from '@/helpers/utils.js'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state' import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue' import { getCurrentWindow } from '@tauri-apps/api/window'
import { RouterView, useRoute, useRouter } from 'vue-router' import { getVersion } from '@tauri-apps/api/app'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { create_profile_and_install_from_file } from './helpers/pack' import { create_profile_and_install_from_file } from './helpers/pack'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' import { useError } from '@/store/error.js'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import { useInstall } from '@/store/install.js'
import { invoke } from '@tauri-apps/api/core'
import { get_opening_command, initialize_state } from '@/helpers/state'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { renderString } from '@modrinth/utils'
import { useFetch } from '@/helpers/fetch.js'
import { check } from '@tauri-apps/plugin-updater'
import NavButton from '@/components/ui/NavButton.vue'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
import { get_user } from '@/helpers/cache.js'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import { get_available_capes, get_available_skins } from './helpers/skins' import { get_available_capes, get_available_skins } from './helpers/skins'
import { AppNotificationManager } from './providers/app-notifications' import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
const themeStore = useTheming() const themeStore = useTheming()
const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
const { handleError, addNotification } = notificationManager
const news = ref([]) const news = ref([])
const urlModal = ref(null) const urlModal = ref(null)
@@ -172,7 +167,7 @@ async function setupApp() {
} }
await warning_listener((e) => await warning_listener((e) =>
addNotification({ notificationsWrapper.value.addNotification({
title: 'Warning', title: 'Warning',
text: e.message, text: e.message,
type: 'warn', type: 'warn',
@@ -256,6 +251,9 @@ const route = useRoute()
const loading = useLoading() const loading = useLoading()
loading.setEnabled(false) loading.setEnabled(false)
const notifications = useNotifications()
const notificationsWrapper = ref()
const error = useError() const error = useError()
const errorModal = ref() const errorModal = ref()
@@ -337,6 +335,8 @@ watch(
onMounted(() => { onMounted(() => {
invoke('show_window') invoke('show_window')
notifications.setNotifs(notificationsWrapper.value)
error.setErrorModal(errorModal.value) error.setErrorModal(errorModal.value)
install.setIncompatibilityWarningModal(incompatibilityWarningModal) install.setIncompatibilityWarningModal(incompatibilityWarningModal)
@@ -657,7 +657,7 @@ function handleAuxClick(e) {
</div> </div>
</div> </div>
<URLConfirmModal ref="urlModal" /> <URLConfirmModal ref="urlModal" />
<NotificationPanel has-sidebar /> <Notifications ref="notificationsWrapper" sidebar />
<ErrorModal ref="errorModal" /> <ErrorModal ref="errorModal" />
<ModInstallModal ref="modInstallModal" /> <ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" /> <IncompatibilityWarningModal ref="incompatibilityWarningModal" />
@@ -1,25 +1,24 @@
<script setup> <script setup>
import ContextMenu from '@/components/ui/ContextMenu.vue'
import Instance from '@/components/ui/Instance.vue' import Instance from '@/components/ui/Instance.vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue' import { computed, ref } from 'vue'
import { duplicate, remove } from '@/helpers/profile.js'
import { import {
ClipboardCopyIcon, ClipboardCopyIcon,
EyeIcon,
FolderOpenIcon, FolderOpenIcon,
PlayIcon, PlayIcon,
PlusIcon, PlusIcon,
SearchIcon,
StopCircleIcon,
TrashIcon, TrashIcon,
StopCircleIcon,
EyeIcon,
SearchIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui' import { Button, DropdownSelect } from '@modrinth/ui'
import { formatCategoryHeader } from '@modrinth/utils' import { formatCategoryHeader } from '@modrinth/utils'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, ref } from 'vue' import { duplicate, remove } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
const { handleError } = injectNotificationManager() import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
const props = defineProps({ const props = defineProps({
instances: { instances: {
+18 -19
View File
@@ -1,32 +1,31 @@
<script setup> <script setup>
import ContextMenu from '@/components/ui/ContextMenu.vue'
import Instance from '@/components/ui/Instance.vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import ProjectCard from '@/components/ui/ProjectCard.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_by_profile_path } from '@/helpers/process.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js'
import { import {
ClipboardCopyIcon, ClipboardCopyIcon,
DownloadIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon, FolderOpenIcon,
GlobeIcon,
PlayIcon, PlayIcon,
PlusIcon, PlusIcon,
StopCircleIcon,
TrashIcon, TrashIcon,
DownloadIcon,
GlobeIcon,
StopCircleIcon,
ExternalIcon,
EyeIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { HeadingLink, injectNotificationManager } from '@modrinth/ui' import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { openUrl } from '@tauri-apps/plugin-opener' import Instance from '@/components/ui/Instance.vue'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import ProjectCard from '@/components/ui/ProjectCard.vue'
import { get_by_profile_path } from '@/helpers/process.js'
import { handleError } from '@/store/notifications.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showProfileInFolder } from '@/helpers/utils.js'
const { handleError } = injectNotificationManager() import { trackEvent } from '@/helpers/analytics'
import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js'
import { openUrl } from '@tauri-apps/plugin-opener'
import { HeadingLink } from '@modrinth/ui'
const router = useRouter() const router = useRouter()
@@ -73,23 +73,22 @@
</template> </template>
<script setup> <script setup>
import { trackEvent } from '@/helpers/analytics' import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon, SpinnerIcon } from '@modrinth/assets'
import { Avatar, Button, Card } from '@modrinth/ui'
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
import { import {
get_default_user, users,
login as login_flow,
remove_user, remove_user,
set_default_user, set_default_user,
users, login as login_flow,
get_default_user,
} from '@/helpers/auth' } from '@/helpers/auth'
import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
import { get_available_skins } from '@/helpers/skins'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import { DropdownIcon, LogInIcon, PlusIcon, SpinnerIcon, TrashIcon } from '@modrinth/assets' import { get_available_skins } from '@/helpers/skins'
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui' import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
const { handleError } = injectNotificationManager()
defineProps({ defineProps({
mode: { mode: {
@@ -1,12 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { add_project_from_path } from '@/helpers/profile.js' import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets' import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { add_project_from_path } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const { handleError } = injectNotificationManager()
const props = defineProps({ const props = defineProps({
instance: { instance: {
type: Object, type: Object,
@@ -1,24 +1,23 @@
<script setup> <script setup>
import { ChatIcon } from '@/assets/icons'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { install } from '@/helpers/profile.js'
import { cancel_directory_change } from '@/helpers/settings.ts'
import { handleSevereError } from '@/store/error.js'
import { import {
CheckIcon, CheckIcon,
CopyIcon,
DropdownIcon, DropdownIcon,
XIcon,
HammerIcon, HammerIcon,
LogInIcon, LogInIcon,
UpdatedIcon, UpdatedIcon,
XIcon, CopyIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui' import { ChatIcon } from '@/assets/icons'
import { computed, ref } from 'vue' import { ButtonStyled, Collapsible } from '@modrinth/ui'
import { ref, computed } from 'vue'
const { handleError } = injectNotificationManager() import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleError } from '@/store/notifications.js'
import { handleSevereError } from '@/store/error.js'
import { cancel_directory_change } from '@/helpers/settings.ts'
import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref() const errorModal = ref()
const error = ref() const error = ref()
@@ -1,13 +1,12 @@
<script setup> <script setup>
import { XIcon, PlusIcon } from '@modrinth/assets'
import { Button, Checkbox } from '@modrinth/ui'
import { PackageIcon, VersionIcon } from '@/assets/icons' import { PackageIcon, VersionIcon } from '@/assets/icons'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
import { PlusIcon, XIcon } from '@modrinth/assets'
import { Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref } from 'vue' import { ref } from 'vue'
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
const { handleError } = injectNotificationManager() import { open } from '@tauri-apps/plugin-dialog'
import { handleError } from '@/store/notifications.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -1,10 +1,6 @@
<script setup> <script setup>
import { trackEvent } from '@/helpers/analytics' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { process_listener } from '@/helpers/events' import { useRouter } from 'vue-router'
import { get_by_profile_path } from '@/helpers/process'
import { finish_install, kill, run } from '@/helpers/profile'
import { showProfileInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
import { import {
DownloadIcon, DownloadIcon,
GameIcon, GameIcon,
@@ -13,13 +9,17 @@ import {
StopCircleIcon, StopCircleIcon,
TimerIcon, TimerIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui' import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { finish_install, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
import { process_listener } from '@/helpers/events'
import { handleError } from '@/store/state.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
import { trackEvent } from '@/helpers/analytics'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const { handleError } = injectNotificationManager()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const props = defineProps({ const props = defineProps({
@@ -198,17 +198,6 @@
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { trackEvent } from '@/helpers/analytics'
import {
get_default_launcher_path,
get_importable_instances,
import_instance,
} from '@/helpers/import.js'
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
import { create } from '@/helpers/profile'
import { get_loaders } from '@/helpers/tags'
import { import {
CodeIcon, CodeIcon,
FolderOpenIcon, FolderOpenIcon,
@@ -219,14 +208,24 @@ import {
UploadIcon, UploadIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, Checkbox, Chips, injectNotificationManager } from '@modrinth/ui' import { Avatar, Button, Checkbox, Chips } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, onUnmounted, ref, shallowRef } from 'vue' import { computed, onUnmounted, ref, shallowRef } from 'vue'
import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile'
import { open } from '@tauri-apps/plugin-dialog'
import { convertFileSrc } from '@tauri-apps/api/core'
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import { trackEvent } from '@/helpers/analytics'
const { handleError } = injectNotificationManager() import { create_profile_and_install_from_file } from '@/helpers/pack.js'
import {
get_default_launcher_path,
get_importable_instances,
import_instance,
} from '@/helpers/import.js'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { getCurrentWebview } from '@tauri-apps/api/webview'
const profile_name = ref('') const profile_name = ref('')
const game_version = ref('') const game_version = ref('')
@@ -35,14 +35,13 @@
</ModalWrapper> </ModalWrapper>
</template> </template>
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
import { trackEvent } from '@/helpers/analytics' import { Button } from '@modrinth/ui'
import { find_filtered_jres } from '@/helpers/jre.js'
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import { find_filtered_jres } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager() import { handleError } from '@/store/notifications.js'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const chosenInstallOptions = ref([]) const chosenInstallOptions = ref([])
const detectJavaModal = ref(null) const detectJavaModal = ref(null)
@@ -52,22 +52,21 @@
</template> </template>
<script setup> <script setup>
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
import { import {
CheckIcon,
DownloadIcon,
FolderSearchIcon,
PlayIcon,
SearchIcon, SearchIcon,
PlayIcon,
CheckIcon,
XIcon, XIcon,
FolderSearchIcon,
DownloadIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui' import { Button } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog' import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
import { ref } from 'vue' import { ref } from 'vue'
import { open } from '@tauri-apps/plugin-dialog'
const { handleError } = injectNotificationManager() import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
const props = defineProps({ const props = defineProps({
version: { version: {
@@ -1,14 +1,13 @@
<script setup> <script setup>
import NavButton from '@/components/ui/NavButton.vue'
import { profile_listener } from '@/helpers/events.js'
import { list } from '@/helpers/profile' import { list } from '@/helpers/profile'
import { SpinnerIcon } from '@modrinth/assets' import { handleError } from '@/store/notifications'
import { Avatar, injectNotificationManager } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { onUnmounted, ref } from 'vue' import { onUnmounted, ref } from 'vue'
import { profile_listener } from '@/helpers/events.js'
const { handleError } = injectNotificationManager() import NavButton from '@/components/ui/NavButton.vue'
import { Avatar } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { SpinnerIcon } from '@modrinth/assets'
const recentInstances = ref([]) const recentInstances = ref([])
const getInstances = async () => { const getInstances = async () => {
@@ -94,24 +94,23 @@
</template> </template>
<script setup> <script setup>
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { trackEvent } from '@/helpers/analytics'
import { loading_listener, process_listener } from '@/helpers/events'
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
import { get_many } from '@/helpers/profile.js'
import { progress_bars_list } from '@/helpers/state.js'
import { import {
DownloadIcon, DownloadIcon,
DropdownIcon,
StopCircleIcon, StopCircleIcon,
TerminalSquareIcon, TerminalSquareIcon,
DropdownIcon,
UnplugIcon, UnplugIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui' import { Button, ButtonStyled, Card } from '@modrinth/ui'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
import { loading_listener, process_listener } from '@/helpers/events'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { progress_bars_list } from '@/helpers/state.js'
const { handleError } = injectNotificationManager() import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js'
import { get_many } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
const router = useRouter() const router = useRouter()
const card = ref(null) const card = ref(null)
@@ -1,13 +1,12 @@
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { Button } from '@modrinth/ui'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get_project, get_version } from '@/helpers/cache.js'
import { get_categories } from '@/helpers/tags.js'
import { install as installVersion } from '@/store/install.js'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import SearchCard from '@/components/ui/SearchCard.vue'
const { handleError } = injectNotificationManager() import { get_categories } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js'
import { get_version, get_project } from '@/helpers/cache.js'
import { install as installVersion } from '@/store/install.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const confirmModal = ref(null) const confirmModal = ref(null)
const project = ref(null) const project = ref(null)
@@ -1,29 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import ContextMenu from '@/components/ui/ContextMenu.vue' import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get_user_many } from '@/helpers/cache'
import { friend_listener } from '@/helpers/events'
import { add_friend, friend_statuses, friends, remove_friend } from '@/helpers/friends'
import { import {
MailIcon, UserPlusIcon,
MoreVerticalIcon, MoreVerticalIcon,
MailIcon,
SettingsIcon, SettingsIcon,
TrashIcon, TrashIcon,
UserPlusIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { import { ref, onUnmounted, watch, computed } from 'vue'
Avatar, import { friend_listener } from '@/helpers/events'
ButtonStyled, import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
injectNotificationManager, import { get_user_many } from '@/helpers/cache'
OverflowMenu, import { handleError } from '@/store/notifications.js'
useRelativeTime, import ContextMenu from '@/components/ui/ContextMenu.vue'
} from '@modrinth/ui'
import type { Dayjs } from 'dayjs' import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, onUnmounted, ref, watch } from 'vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const { handleError } = injectNotificationManager()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const props = defineProps<{ const props = defineProps<{
@@ -57,16 +57,15 @@
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics' import { XIcon, DownloadIcon } from '@modrinth/assets'
import { add_project_from_version as installMod } from '@/helpers/profile' import { Button } from '@modrinth/ui'
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils' import { formatCategory } from '@modrinth/utils'
import { add_project_from_version as installMod } from '@/helpers/profile'
import { ref } from 'vue' import { ref } from 'vue'
import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
const { handleError } = injectNotificationManager()
const instance = ref(null) const instance = ref(null)
const project = ref(null) const project = ref(null)
const versions = ref(null) const versions = ref(null)
@@ -77,10 +76,10 @@ const installing = ref(false)
const onInstall = ref(() => {}) const onInstall = ref(() => {})
defineExpose({ defineExpose({
show: (instanceVal, projectVal, projectVersions, callback) => { show: (instanceVal, projectVal, projectVersions, selected, callback) => {
instance.value = instanceVal instance.value = instanceVal
versions.value = projectVersions versions.value = projectVersions
selectedVersion.value = projectVersions[0] selectedVersion.value = selected ?? projectVersions[0]
project.value = projectVal project.value = projectVal
@@ -1,12 +1,11 @@
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { create_profile_and_install as pack_install } from '@/helpers/pack'
import { DownloadIcon, XIcon } from '@modrinth/assets' import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui' import { Button } from '@modrinth/ui'
import { create_profile_and_install as pack_install } from '@/helpers/pack'
import { ref } from 'vue' import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
const { handleError } = injectNotificationManager() import { handleError } from '@/store/state.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const versionId = ref() const versionId = ref()
const project = ref() const project = ref()
@@ -1,29 +1,29 @@
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { import {
check_installed,
create,
get,
add_project_from_version as installMod,
list,
} from '@/helpers/profile'
import { installVersionDependencies } from '@/store/install.js'
import {
CheckIcon,
DownloadIcon, DownloadIcon,
PlusIcon, PlusIcon,
RightArrowIcon,
UploadIcon, UploadIcon,
XIcon, XIcon,
RightArrowIcon,
CheckIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui' import { Avatar, Button, Card } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import {
add_project_from_version as installMod,
check_installed,
get,
list,
create,
} from '@/helpers/profile'
import { open } from '@tauri-apps/plugin-dialog'
import { installVersionDependencies } from '@/store/install.js'
import { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { convertFileSrc } from '@tauri-apps/api/core'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const { handleError } = injectNotificationManager()
const router = useRouter() const router = useRouter()
const versions = ref() const versions = ref()
@@ -1,23 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
Checkbox,
injectNotificationManager,
OverflowMenu,
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
import { computed, ref, type Ref, watch } from 'vue'
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
import { handleError } from '@/store/notifications'
import { trackEvent } from '@/helpers/analytics'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { defineMessages, useVIntl } from '@vintl/vintl' import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref, type Ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types' import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const router = useRouter() const router = useRouter()
@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { edit } from '@/helpers/profile' import { Checkbox } from '@modrinth/ui'
import { get } from '@/helpers/settings.ts'
import { Checkbox, injectNotificationManager } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types' import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { get } from '@/helpers/settings.ts'
import { edit } from '@/helpers/profile'
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>() const props = defineProps<InstanceSettingsTabProps>()
@@ -1,30 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project, get_version_many } from '@/helpers/cache'
import { get_loader_versions } from '@/helpers/metadata'
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { import {
DownloadIcon,
HammerIcon,
IssuesIcon,
SpinnerIcon,
TransferIcon, TransferIcon,
UndoIcon, IssuesIcon,
UnlinkIcon, HammerIcon,
UnplugIcon, DownloadIcon,
WrenchIcon, WrenchIcon,
UndoIcon,
SpinnerIcon,
UnplugIcon,
UnlinkIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { import { Avatar, Checkbox, Chips, ButtonStyled, TeleportDropdownMenu } from '@modrinth/ui'
Avatar, import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
ButtonStyled, import { edit, install, update_repair_modrinth } from '@/helpers/profile'
Checkbox, import { handleError } from '@/store/notifications'
Chips, import { trackEvent } from '@/helpers/analytics'
TeleportDropdownMenu, import { defineMessages, useVIntl } from '@vintl/vintl'
injectNotificationManager, import { get_loader_versions } from '@/helpers/metadata'
} from '@modrinth/ui' import { get_game_versions, get_loaders } from '@/helpers/tags'
import { import {
formatCategory, formatCategory,
type GameVersionTag, type GameVersionTag,
@@ -32,16 +25,16 @@ import {
type Project, type Project,
type Version, type Version,
} from '@modrinth/utils' } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl' import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { get_project, get_version_many } from '@/helpers/cache'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, ref, shallowRef, watch, type ComputedRef, type Ref } from 'vue'
import type { import type {
InstanceSettingsTabProps, InstanceSettingsTabProps,
Manifest,
ManifestLoaderVersion, ManifestLoaderVersion,
Manifest,
} from '../../../helpers/types' } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const repairConfirmModal = ref() const repairConfirmModal = ref()
@@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import JavaSelector from '@/components/ui/JavaSelector.vue' import { Checkbox, Slider } from '@modrinth/ui'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets' import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
import { Checkbox, injectNotificationManager, Slider } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, readonly, ref, watch } from 'vue' import { computed, readonly, ref, watch } from 'vue'
import type { AppSettings, InstanceSettingsTabProps, MemorySettings } from '../../../helpers/types' import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { get } from '@/helpers/settings.ts'
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
import useMemorySlider from '@/composables/useMemorySlider'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>() const props = defineProps<InstanceSettingsTabProps>()
@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { edit } from '@/helpers/profile' import { Checkbox, Toggle } from '@modrinth/ui'
import { get } from '@/helpers/settings.ts'
import { Checkbox, injectNotificationManager, Toggle } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref, type Ref, watch } from 'vue' import { computed, ref, type Ref, watch } from 'vue'
import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { get } from '@/helpers/settings.ts'
import { edit } from '@/helpers/profile'
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types' import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>() const props = defineProps<InstanceSettingsTabProps>()
@@ -1,10 +1,8 @@
<script setup> <script setup>
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { get_java_versions, set_java_version } from '@/helpers/jre'
import { injectNotificationManager } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import { get_java_versions, set_java_version } from '@/helpers/jre'
const { handleError } = injectNotificationManager() import { handleError } from '@/store/notifications'
import JavaSelector from '@/components/ui/JavaSelector.vue'
const javaVersions = ref(await get_java_versions().catch(handleError)) const javaVersions = ref(await get_java_versions().catch(handleError))
async function updateJavaVersion(version) { async function updateJavaVersion(version) {
@@ -1,13 +1,13 @@
<script setup> <script setup>
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue' import { Button, Slider } from '@modrinth/ui'
import { purge_cache_types } from '@/helpers/cache.js'
import { get, set } from '@/helpers/settings.ts'
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { Button, Slider, injectNotificationManager } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { purge_cache_types } from '@/helpers/cache.js'
import { handleError } from '@/store/notifications.js'
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { open } from '@tauri-apps/plugin-dialog'
const { handleError } = injectNotificationManager()
const settings = ref(await get()) const settings = ref(await get())
watch( watch(
@@ -100,39 +100,37 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { ref, computed, watch, useTemplateRef } from 'vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue' import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
import {
add_and_equip_custom_skin,
determineModelType,
get_normalized_skin_texture,
remove_custom_skin,
unequip_skin,
type Cape,
type Skin,
type SkinModel,
} from '@/helpers/skins.ts'
import {
CheckIcon,
ChevronRightIcon,
SaveIcon,
SpinnerIcon,
UploadIcon,
XIcon,
} from '@modrinth/assets'
import { import {
SkinPreviewRenderer,
Button, Button,
ButtonStyled, RadioButtons,
CapeButton, CapeButton,
CapeLikeTextButton, CapeLikeTextButton,
injectNotificationManager, ButtonStyled,
RadioButtons,
SkinPreviewRenderer,
} from '@modrinth/ui' } from '@modrinth/ui'
import { computed, ref, useTemplateRef, watch } from 'vue' import {
add_and_equip_custom_skin,
const { handleError } = injectNotificationManager() remove_custom_skin,
unequip_skin,
type Skin,
type Cape,
type SkinModel,
get_normalized_skin_texture,
determineModelType,
} from '@/helpers/skins.ts'
import { handleError } from '@/store/notifications'
import {
UploadIcon,
CheckIcon,
SaveIcon,
XIcon,
ChevronRightIcon,
SpinnerIcon,
} from '@modrinth/assets'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
const modal = useTemplateRef('modal') const modal = useTemplateRef('modal')
const selectCapeModal = useTemplateRef('selectCapeModal') const selectCapeModal = useTemplateRef('selectCapeModal')
@@ -27,14 +27,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onBeforeUnmount, watch } from 'vue'
import { UploadIcon } from '@modrinth/assets'
import { useNotifications } from '@/store/state'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get_dragged_skin_data } from '@/helpers/skins' import { get_dragged_skin_data } from '@/helpers/skins'
import { UploadIcon } from '@modrinth/assets'
import { injectNotificationManager } from '@modrinth/ui'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { onBeforeUnmount, ref, watch } from 'vue'
const { addNotification } = injectNotificationManager() const notifications = useNotifications()
const modal = ref() const modal = ref()
const fileInput = ref<HTMLInputElement>() const fileInput = ref<HTMLInputElement>()
@@ -99,7 +99,7 @@ async function setupDragDropListener() {
const data = await get_dragged_skin_data(filePath) const data = await get_dragged_skin_data(filePath)
await processData(data.buffer) await processData(data.buffer)
} catch (error) { } catch (error) {
addNotification({ notifications.addNotification({
title: 'Error processing file', title: 'Error processing file',
text: error instanceof Error ? error.message : 'Failed to read the dropped file.', text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
type: 'error', type: 'error',
@@ -1,12 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { trackEvent } from '@/helpers/analytics' import type { Dayjs } from 'dayjs'
import { get_project } from '@/helpers/cache' import dayjs from 'dayjs'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { kill, run } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
import { showProfileInFolder } from '@/helpers/utils'
import { handleSevereError } from '@/store/error'
import { import {
EyeIcon, EyeIcon,
FolderOpenIcon, FolderOpenIcon,
@@ -19,20 +13,25 @@ import {
Avatar, Avatar,
ButtonStyled, ButtonStyled,
commonMessages, commonMessages,
injectNotificationManager,
OverflowMenu, OverflowMenu,
SmartClickable, SmartClickable,
useRelativeTime, useRelativeTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { capitalizeString } from '@modrinth/utils'
import { convertFileSrc } from '@tauri-apps/api/core'
import { useVIntl } from '@vintl/vintl' import { useVIntl } from '@vintl/vintl'
import type { Dayjs } from 'dayjs' import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
import dayjs from 'dayjs' import { showProfileInFolder } from '@/helpers/utils'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue' import { convertFileSrc } from '@tauri-apps/api/core'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { GameInstance } from '@/helpers/types'
import { get_project } from '@/helpers/cache'
import { capitalizeString } from '@modrinth/utils'
import { kill, run } from '@/helpers/profile'
import { handleSevereError } from '@/store/error'
import { trackEvent } from '@/helpers/analytics'
import { get_by_profile_path } from '@/helpers/process'
import { handleError } from '@/store/notifications'
import { process_listener } from '@/helpers/events'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
@@ -1,31 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { trackEvent } from '@/helpers/analytics'
import { process_listener, profile_listener } from '@/helpers/events'
import { get_all } from '@/helpers/process'
import { kill, run } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
import { import {
type ProtocolVersion, type ProtocolVersion,
type ServerData,
type ServerWorld, type ServerWorld,
type ServerData,
type WorldWithProfile, type WorldWithProfile,
get_recent_worlds,
getWorldIdentifier, getWorldIdentifier,
get_profile_protocol_version, get_profile_protocol_version,
get_recent_worlds,
refreshServerData, refreshServerData,
start_join_server, start_join_server,
start_join_singleplayer_world, start_join_singleplayer_world,
} from '@/helpers/worlds.ts' } from '@/helpers/worlds.ts'
import { handleSevereError } from '@/store/error' import { HeadingLink, GAME_MODES } from '@modrinth/ui'
import { useTheming } from '@/store/theme.ts' import WorldItem from '@/components/ui/world/WorldItem.vue'
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui' import InstanceItem from '@/components/ui/world/InstanceItem.vue'
import { watch, onMounted, onUnmounted, ref, computed } from 'vue'
import type { Dayjs } from 'dayjs' import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { useTheming } from '@/store/theme.ts'
import { kill, run } from '@/helpers/profile'
const { handleError } = injectNotificationManager() import { handleError } from '@/store/notifications'
import { trackEvent } from '@/helpers/analytics'
import { process_listener, profile_listener } from '@/helpers/events'
import { get_all } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { handleSevereError } from '@/store/error'
const props = defineProps<{ const props = defineProps<{
recentInstances: GameInstance[] recentInstances: GameInstance[]
@@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets' import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui' import { ButtonStyled, commonMessages } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { ref } from 'vue' import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import type { GameInstance } from '@/helpers/types'
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { handleError } from '@/store/notifications'
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -1,21 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { SaveIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages } from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import { import {
type ServerPackStatus,
edit_server_in_profile, edit_server_in_profile,
type ServerWorld,
set_world_display_status, set_world_display_status,
type DisplayStatus, type DisplayStatus,
type ServerPackStatus,
type ServerWorld,
} from '@/helpers/worlds.ts' } from '@/helpers/worlds.ts'
import { SaveIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
import { defineMessage, useVIntl } from '@vintl/vintl' import { defineMessage, useVIntl } from '@vintl/vintl'
import { computed, ref } from 'vue' import { handleError } from '@/store/notifications'
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts' import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
import { rename_world, reset_world_icon, set_world_display_status } from '@/helpers/worlds.ts' import { set_world_display_status, rename_world, reset_world_icon } from '@/helpers/worlds.ts'
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl' import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref } from 'vue' import { handleError } from '@/store/notifications'
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -1,9 +1,8 @@
import { ref, computed } from 'vue'
import { get_max_memory } from '@/helpers/jre.js' import { get_max_memory } from '@/helpers/jre.js'
import { injectNotificationManager } from '@modrinth/ui' import { handleError } from '@/store/notifications.js'
import { computed, ref } from 'vue'
export default async function () { export default async function () {
const { handleError } = injectNotificationManager()
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024)) const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
const snapPoints = computed(() => { const snapPoints = computed(() => {
+2 -3
View File
@@ -1,6 +1,6 @@
import { injectNotificationManager } from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app'
import { fetch } from '@tauri-apps/plugin-http' import { fetch } from '@tauri-apps/plugin-http'
import { handleError } from '@/store/state.js'
import { getVersion } from '@tauri-apps/api/app'
export const useFetch = async (url, item, isSilent) => { export const useFetch = async (url, item, isSilent) => {
try { try {
@@ -11,7 +11,6 @@ export const useFetch = async (url, item, isSilent) => {
}) })
} catch (err) { } catch (err) {
if (!isSilent) { if (!isSilent) {
const { handleError } = injectNotificationManager()
handleError({ message: `Error fetching ${item}` }) handleError({ message: `Error fetching ${item}` })
} }
console.error(err) console.error(err)
+2 -3
View File
@@ -3,9 +3,9 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { install_to_existing_profile } from '@/helpers/pack.js'
import { injectNotificationManager } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { install_to_existing_profile } from '@/helpers/pack.js'
import { handleError } from '@/store/notifications.js'
/// Add instance /// Add instance
/* /*
@@ -190,7 +190,6 @@ export async function edit_icon(path, iconPath) {
} }
export async function finish_install(instance) { export async function finish_install(instance) {
const { handleError } = injectNotificationManager()
if (instance.install_stage !== 'pack_installed') { if (instance.install_stage !== 'pack_installed') {
let linkedData = instance.linked_data let linkedData = instance.linked_data
await install_to_existing_profile( await install_to_existing_profile(
+2 -3
View File
@@ -1,6 +1,6 @@
import { injectNotificationManager } from '@modrinth/ui'
import { arrayBufferToBase64 } from '@modrinth/utils'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { handleError } from '@/store/notifications'
import { arrayBufferToBase64 } from '@modrinth/utils'
export interface Cape { export interface Cape {
id: string id: string
@@ -39,7 +39,6 @@ export const DEFAULT_MODELS: Record<string, SkinModel> = {
export function filterSavedSkins(list: Skin[]) { export function filterSavedSkins(list: Skin[]) {
const customSkins = list.filter((s) => s.source !== 'default') const customSkins = list.filter((s) => s.source !== 'default')
const { handleError } = injectNotificationManager()
fixUnknownSkins(customSkins).catch(handleError) fixUnknownSkins(customSkins).catch(handleError)
return customSkins return customSkins
} }
+6 -6
View File
@@ -1,12 +1,12 @@
import App from '@/App.vue' import { createApp } from 'vue'
import router from '@/routes' import router from '@/routes'
import * as Sentry from '@sentry/vue' import App from '@/App.vue'
import { VueScanPlugin } from '@taijased/vue-render-tracker' import { createPinia } from 'pinia'
import { createPlugin } from '@vintl/vintl/plugin'
import FloatingVue from 'floating-vue' import FloatingVue from 'floating-vue'
import 'floating-vue/dist/style.css' import 'floating-vue/dist/style.css'
import { createPinia } from 'pinia' import { createPlugin } from '@vintl/vintl/plugin'
import { createApp } from 'vue' import * as Sentry from '@sentry/vue'
import { VueScanPlugin } from '@taijased/vue-render-tracker'
const VIntlPlugin = createPlugin({ const VIntlPlugin = createPlugin({
controllerOpts: { controllerOpts: {
+17 -18
View File
@@ -1,34 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import ContextMenu from '@/components/ui/ContextMenu.vue' import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import type Instance from '@/components/ui/Instance.vue' import type { Ref } from 'vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue' import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets'
import NavTabs from '@/components/ui/NavTabs.vue'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get_search_results } from '@/helpers/cache.js'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, SearchIcon, XIcon } from '@modrinth/assets'
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui' import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
import { import {
SearchFilterControl,
SearchSidebarFilter,
Button, Button,
Checkbox, Checkbox,
DropdownSelect, DropdownSelect,
injectNotificationManager,
LoadingIndicator, LoadingIndicator,
Pagination, Pagination,
SearchFilterControl,
SearchSidebarFilter,
useSearch, useSearch,
} from '@modrinth/ui' } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener' import { handleError } from '@/store/state'
import { defineMessages, useVIntl } from '@vintl/vintl' import { useBreadcrumbs } from '@/store/breadcrumbs'
import type { Ref } from 'vue' import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import type { LocationQuery } from 'vue-router' import type { LocationQuery } from 'vue-router'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { get_search_results } from '@/helpers/cache.js'
import NavTabs from '@/components/ui/NavTabs.vue'
import type Instance from '@/components/ui/Instance.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import { defineMessages, useVIntl } from '@vintl/vintl'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const router = useRouter() const router = useRouter()
+11 -12
View File
@@ -1,18 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import RowDisplay from '@/components/RowDisplay.vue' import { ref, onUnmounted, computed } from 'vue'
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
import { get_search_results } from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events'
import { list } from '@/helpers/profile.js'
import type { GameInstance } from '@/helpers/types'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { injectNotificationManager } from '@modrinth/ui'
import type { SearchResult } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js'
import { profile_listener } from '@/helpers/events'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs'
import { get_search_results } from '@/helpers/cache.js'
import type { SearchResult } from '@modrinth/utils'
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
import type { GameInstance } from '@/helpers/types'
const { handleError } = injectNotificationManager()
const route = useRoute() const route = useRoute()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
+32 -33
View File
@@ -1,26 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type AccountsCard from '@/components/ui/AccountsCard.vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
import { get as getSettings } from '@/helpers/settings.ts'
import type { Cape, Skin } from '@/helpers/skins.ts'
import {
equip_skin,
filterDefaultSkins,
filterSavedSkins,
get_available_capes,
get_available_skins,
get_normalized_skin_texture,
normalize_skin_texture,
remove_custom_skin,
set_default_cape,
} from '@/helpers/skins.ts'
import { handleSevereError } from '@/store/error'
import { import {
EditIcon, EditIcon,
ExcitedRinthbot, ExcitedRinthbot,
@@ -34,21 +12,42 @@ import {
Button, Button,
ButtonStyled, ButtonStyled,
ConfirmModal, ConfirmModal,
injectNotificationManager,
SkinButton, SkinButton,
SkinLikeTextButton, SkinLikeTextButton,
SkinPreviewRenderer, SkinPreviewRenderer,
} from '@modrinth/ui' } from '@modrinth/ui'
import { arrayBufferToBase64 } from '@modrinth/utils'
import { computedAsync } from '@vueuse/core' import { computedAsync } from '@vueuse/core'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue' import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
import { handleError, useNotifications } from '@/store/notifications'
import type { Cape, Skin } from '@/helpers/skins.ts'
import {
normalize_skin_texture,
equip_skin,
filterDefaultSkins,
filterSavedSkins,
get_available_capes,
get_available_skins,
get_normalized_skin_texture,
remove_custom_skin,
set_default_cape,
} from '@/helpers/skins.ts'
import { get as getSettings } from '@/helpers/settings.ts'
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
import { handleSevereError } from '@/store/error'
import { trackEvent } from '@/helpers/analytics'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
import { arrayBufferToBase64 } from '@modrinth/utils'
const editSkinModal = useTemplateRef('editSkinModal') const editSkinModal = useTemplateRef('editSkinModal')
const selectCapeModal = useTemplateRef('selectCapeModal') const selectCapeModal = useTemplateRef('selectCapeModal')
const uploadSkinModal = useTemplateRef('uploadSkinModal') const uploadSkinModal = useTemplateRef('uploadSkinModal')
const notifications = injectNotificationManager() const notifications = useNotifications()
const { handleError } = notifications
const settings = ref(await getSettings()) const settings = ref(await getSettings())
const skins = ref<Skin[]>([]) const skins = ref<Skin[]>([])
@@ -114,7 +113,7 @@ async function loadCapes() {
defaultCape.value = capes.value.find((c) => c.is_equipped) defaultCape.value = capes.value.find((c) => c.is_equipped)
originalDefaultCape.value = defaultCape.value originalDefaultCape.value = defaultCape.value
} catch (error) { } catch (error) {
if (currentUser.value && error instanceof Error) { if (currentUser.value) {
handleError(error) handleError(error)
} }
} }
@@ -127,7 +126,7 @@ async function loadSkins() {
selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
originalSelectedSkin.value = selectedSkin.value originalSelectedSkin.value = selectedSkin.value
} catch (error) { } catch (error) {
if (currentUser.value && error instanceof Error) { if (currentUser.value) {
handleError(error) handleError(error)
} }
} }
@@ -162,7 +161,7 @@ async function changeSkin(newSkin: Skin) {
text: "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.", text: "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
}) })
} else { } else {
handleError(error as Error) handleError(error)
} }
} }
} }
@@ -191,7 +190,7 @@ async function handleCapeSelected(cape: Cape | undefined) {
text: "You're changing your cape too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.", text: "You're changing your cape too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
}) })
} else { } else {
handleError(error as Error) handleError(error)
} }
} }
} }
@@ -208,7 +207,7 @@ async function loadCurrentUser() {
const allAccounts = await users() const allAccounts = await users()
currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId) currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId)
} catch (e) { } catch (e) {
handleError(e as Error) handleError(e)
currentUser.value = undefined currentUser.value = undefined
currentUserId.value = undefined currentUserId.value = undefined
} }
@@ -277,7 +276,7 @@ async function checkUserChanges() {
await loadSkins() await loadSkins()
} }
} catch (error) { } catch (error) {
if (currentUser.value && error instanceof Error) { if (currentUser.value) {
handleError(error) handleError(error)
} }
} }
@@ -377,7 +376,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
color="green" color="green"
aria-label="Edit skin" aria-label="Edit skin"
class="pointer-events-auto" class="pointer-events-auto"
@click.stop="(e: MouseEvent) => editSkinModal?.show(e, skin)" @click.stop="(e) => editSkinModal?.show(e, skin)"
> >
<EditIcon /> Edit <EditIcon /> Edit
</Button> </Button>
+21 -23
View File
@@ -157,18 +157,13 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import ContextMenu from '@/components/ui/ContextMenu.vue' import {
import ExportModal from '@/components/ui/ExportModal.vue' Avatar,
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue' ButtonStyled,
import NavTabs from '@/components/ui/NavTabs.vue' ContentPageHeader,
import { trackEvent } from '@/helpers/analytics' LoadingIndicator,
import { get_project, get_version_many } from '@/helpers/cache.js' OverflowMenu,
import { process_listener, profile_listener } from '@/helpers/events' } from '@modrinth/ui'
import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import { showProfileInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
import { useBreadcrumbs, useLoading } from '@/store/state'
import { import {
CheckCircleIcon, CheckCircleIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
@@ -192,25 +187,28 @@ import {
UserPlusIcon, UserPlusIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
Avatar, import { get_by_profile_path } from '@/helpers/process'
ButtonStyled, import { process_listener, profile_listener } from '@/helpers/events'
ContentPageHeader, import { useRoute, useRouter } from 'vue-router'
injectNotificationManager, import { computed, onUnmounted, ref, watch } from 'vue'
LoadingIndicator, import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
OverflowMenu, import { showProfileInFolder } from '@/helpers/utils.js'
} from '@modrinth/ui' import ContextMenu from '@/components/ui/ContextMenu.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { trackEvent } from '@/helpers/analytics'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { handleSevereError } from '@/store/error.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration' import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { computed, onUnmounted, ref, watch } from 'vue' import ExportModal from '@/components/ui/ExportModal.vue'
import { useRoute, useRouter } from 'vue-router' import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
dayjs.extend(duration) dayjs.extend(duration)
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const { handleError } = injectNotificationManager()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -88,30 +88,30 @@
</template> </template>
<script setup> <script setup>
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue' import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
import { process_listener } from '@/helpers/events.js' import { Button, Card, Checkbox, DropdownSelect } from '@modrinth/ui'
import { import {
delete_logs_by_filename, delete_logs_by_filename,
get_latest_log_cursor,
get_logs, get_logs,
get_output_by_filename, get_output_by_filename,
get_latest_log_cursor,
} from '@/helpers/logs.js' } from '@/helpers/logs.js'
import { get_by_profile_path } from '@/helpers/process.js' import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
import { Button, Card, Checkbox, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import isToday from 'dayjs/plugin/isToday' import isToday from 'dayjs/plugin/isToday'
import isYesterday from 'dayjs/plugin/isYesterday' import isYesterday from 'dayjs/plugin/isYesterday'
import { ofetch } from 'ofetch' import { get_by_profile_path } from '@/helpers/process.js'
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { process_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js'
import { ofetch } from 'ofetch'
import { RecycleScroller } from 'vue-virtual-scroller' import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
dayjs.extend(isToday) dayjs.extend(isToday)
dayjs.extend(isYesterday) dayjs.extend(isYesterday)
const { handleError } = injectNotificationManager()
const route = useRoute() const route = useRoute()
const props = defineProps({ const props = defineProps({
+29 -31
View File
@@ -249,30 +249,6 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { TextInputIcon } from '@/assets/icons'
import AddContentButton from '@/components/ui/AddContentButton.vue'
import type ContextMenu from '@/components/ui/ContextMenu.vue'
import ExportModal from '@/components/ui/ExportModal.vue'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import { trackEvent } from '@/helpers/analytics'
import {
get_organization_many,
get_project_many,
get_team_many,
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import {
add_project_from_path,
get_projects,
remove_project,
toggle_disable_project,
update_all,
update_project,
} from '@/helpers/profile.js'
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
import { highlightModInProfile } from '@/helpers/utils.js'
import { import {
CheckCircleIcon, CheckCircleIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
@@ -295,22 +271,44 @@ import {
Button, Button,
ButtonStyled, ButtonStyled,
ContentListPanel, ContentListPanel,
injectNotificationManager,
OverflowMenu, OverflowMenu,
Pagination, Pagination,
RadialHeader, RadialHeader,
Toggle, Toggle,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils' import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue' import { computed, onUnmounted, ref, watch } from 'vue'
import { defineMessages, useVIntl } from '@vintl/vintl'
const { handleError } = injectNotificationManager() import {
add_project_from_path,
get_projects,
remove_project,
toggle_disable_project,
update_all,
update_project,
} from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { trackEvent } from '@/helpers/analytics'
import { highlightModInProfile } from '@/helpers/utils.js'
import { TextInputIcon } from '@/assets/icons'
import ExportModal from '@/components/ui/ExportModal.vue'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import AddContentButton from '@/components/ui/AddContentButton.vue'
import {
get_organization_many,
get_project_many,
get_team_many,
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import dayjs from 'dayjs'
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
import type ContextMenu from '@/components/ui/ContextMenu.vue'
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
const props = defineProps<{ const props = defineProps<{
instance: GameInstance instance: GameInstance
+46 -46
View File
@@ -121,55 +121,55 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type ContextMenu from '@/components/ui/ContextMenu.vue' import { ref, computed, onUnmounted, watch } from 'vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue' import { useRoute } from 'vue-router'
import type { GameInstance } from '@/helpers/types'
import {
Button,
ButtonStyled,
RadialHeader,
FilterBar,
type FilterBarOption,
type GameVersion,
GAME_MODES,
} from '@modrinth/ui'
import { PlusIcon, SpinnerIcon, UpdatedIcon, SearchIcon, XIcon } from '@modrinth/assets'
import {
type ProtocolVersion,
type SingleplayerWorld,
type World,
type ServerWorld,
type ServerData,
type ProfileEvent,
get_profile_protocol_version,
remove_server_from_profile,
delete_world,
start_join_server,
start_join_singleplayer_world,
getWorldIdentifier,
refreshServerData,
refreshWorld,
sortWorlds,
refreshServers,
hasWorldQuickPlaySupport,
refreshWorlds,
handleDefaultProfileUpdateEvent,
showWorldInFolder,
hasServerQuickPlaySupport,
} from '@/helpers/worlds.ts'
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue' import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue' import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue' import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue' import WorldItem from '@/components/ui/world/WorldItem.vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { handleError } from '@/store/notifications'
import type ContextMenu from '@/components/ui/ContextMenu.vue'
import type { Version } from '@modrinth/utils'
import { profile_listener } from '@/helpers/events' import { profile_listener } from '@/helpers/events'
import { get_game_versions } from '@/helpers/tags' import { get_game_versions } from '@/helpers/tags'
import type { GameInstance } from '@/helpers/types'
import {
type ProfileEvent,
type ProtocolVersion,
type ServerData,
type ServerWorld,
type SingleplayerWorld,
type World,
delete_world,
getWorldIdentifier,
get_profile_protocol_version,
handleDefaultProfileUpdateEvent,
hasServerQuickPlaySupport,
hasWorldQuickPlaySupport,
refreshServerData,
refreshServers,
refreshWorld,
refreshWorlds,
remove_server_from_profile,
showWorldInFolder,
sortWorlds,
start_join_server,
start_join_singleplayer_world,
} from '@/helpers/worlds.ts'
import { PlusIcon, SearchIcon, SpinnerIcon, UpdatedIcon, XIcon } from '@modrinth/assets'
import {
Button,
ButtonStyled,
FilterBar,
type FilterBarOption,
GAME_MODES,
type GameVersion,
RadialHeader,
injectNotificationManager,
} from '@modrinth/ui'
import type { Version } from '@modrinth/utils'
import { defineMessages } from '@vintl/vintl' import { defineMessages } from '@vintl/vintl'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const { handleError } = injectNotificationManager()
const route = useRoute() const route = useRoute()
const addServerModal = ref<InstanceType<typeof AddServerModal>>() const addServerModal = ref<InstanceType<typeof AddServerModal>>()
@@ -282,7 +282,7 @@ async function editServer(server: ServerWorld) {
await refreshServer(server.address) await refreshServer(server.address)
} }
} else { } else {
handleError(new Error(`Error refreshing server, refreshing all worlds`)) handleError(`Error refreshing server, refreshing all worlds`)
await refreshAllWorlds() await refreshAllWorlds()
} }
} }
@@ -301,7 +301,7 @@ async function editWorld(path: string, name: string, removeIcon: boolean) {
} }
sortWorlds(worlds.value) sortWorlds(worlds.value)
} else { } else {
handleError(new Error(`Error finding world in list, refreshing all worlds`)) handleError(`Error finding world in list, refreshing all worlds`)
await refreshAllWorlds() await refreshAllWorlds()
} }
} }
@@ -311,7 +311,7 @@ async function deleteWorld(world: SingleplayerWorld) {
worlds.value = worlds.value.filter((w) => w.type !== 'singleplayer' || w.path !== world.path) worlds.value = worlds.value.filter((w) => w.type !== 'singleplayer' || w.path !== world.path)
} }
function handleJoinError(err: Error) { function handleJoinError(err: unknown) {
handleError(err) handleError(err)
startingInstance.value = false startingInstance.value = false
worldPlaying.value = undefined worldPlaying.value = undefined
@@ -436,7 +436,7 @@ function promptToRemoveWorld(world: World): boolean {
async function proceedRemoveServer() { async function proceedRemoveServer() {
if (!serverToRemove.value) { if (!serverToRemove.value) {
handleError(new Error(`Error removing server, no server marked for removal.`)) handleError(`Error removing server, no server marked for removal.`)
return return
} }
await removeServer(serverToRemove.value) await removeServer(serverToRemove.value)
@@ -445,7 +445,7 @@ async function proceedRemoveServer() {
async function proceedDeleteWorld() { async function proceedDeleteWorld() {
if (!worldToDelete.value) { if (!worldToDelete.value) {
handleError(new Error(`Error deleting world, no world marked for removal.`)) handleError(`Error deleting world, no world marked for removal.`)
return return
} }
await deleteWorld(worldToDelete.value) await deleteWorld(worldToDelete.value)
@@ -1,16 +1,16 @@
<script setup> <script setup>
import { NewInstanceImage } from '@/assets/icons'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { profile_listener } from '@/helpers/events.js'
import { list } from '@/helpers/profile.js'
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
import { PlusIcon } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { onUnmounted, ref, shallowRef } from 'vue' import { onUnmounted, ref, shallowRef } from 'vue'
import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
import { profile_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js'
import { Button } from '@modrinth/ui'
import { PlusIcon } from '@modrinth/assets'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { NewInstanceImage } from '@/assets/icons'
import NavTabs from '@/components/ui/NavTabs.vue'
const { handleError } = injectNotificationManager()
const route = useRoute() const route = useRoute()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
+23 -23
View File
@@ -129,46 +129,46 @@
</template> </template>
<script setup> <script setup>
import ContextMenu from '@/components/ui/ContextMenu.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { install as installVersion } from '@/store/install.js'
import { useTheming } from '@/store/state.js'
import { import {
BookmarkIcon, BookmarkIcon,
CheckIcon,
ClipboardCopyIcon,
DownloadIcon,
ExternalIcon,
GlobeIcon,
HeartIcon,
MoreVerticalIcon, MoreVerticalIcon,
DownloadIcon,
ReportIcon, ReportIcon,
HeartIcon,
ExternalIcon,
CheckIcon,
GlobeIcon,
ClipboardCopyIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { import {
ButtonStyled,
OverflowMenu,
ProjectBackgroundGradient,
ProjectHeader, ProjectHeader,
ProjectSidebarCompatibility, ProjectSidebarCompatibility,
ButtonStyled,
OverflowMenu,
ProjectSidebarLinks,
ProjectSidebarCreators, ProjectSidebarCreators,
ProjectSidebarDetails, ProjectSidebarDetails,
ProjectSidebarLinks, ProjectBackgroundGradient,
injectNotificationManager,
} from '@modrinth/ui' } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, shallowRef, watch } from 'vue'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { install as installVersion } from '@/store/install.js'
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
import NavTabs from '@/components/ui/NavTabs.vue'
import { useTheming } from '@/store/state.js'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const { handleError } = injectNotificationManager()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
@@ -65,16 +65,12 @@
</template> </template>
<script setup> <script setup>
import { ProjectPageVersions, ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
import { ref } from 'vue'
import { SwapIcon } from '@/assets/icons/index.js' import { SwapIcon } from '@/assets/icons/index.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.js' import { get_game_versions, get_loaders } from '@/helpers/tags.js'
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets' import { handleError } from '@/store/notifications.js'
import {
ButtonStyled,
OverflowMenu,
ProjectPageVersions,
injectNotificationManager,
} from '@modrinth/ui'
import { ref } from 'vue'
defineProps({ defineProps({
project: { project: {
@@ -107,8 +103,6 @@ defineProps({
}, },
}) })
const { handleError } = injectNotificationManager()
const [loaders, gameVersions] = await Promise.all([ const [loaders, gameVersions] = await Promise.all([
get_loaders().catch(handleError).then(ref), get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref), get_game_versions().catch(handleError).then(ref),
@@ -1,48 +0,0 @@
import {
AbstractWebNotificationManager,
type NotificationPanelLocation,
type WebNotification,
} from '@modrinth/ui'
import { ref, type Ref } from 'vue'
export class AppNotificationManager extends AbstractWebNotificationManager {
private readonly state: Ref<WebNotification[]>
private readonly locationState: Ref<NotificationPanelLocation>
public constructor() {
super()
this.state = ref<WebNotification[]>([])
this.locationState = ref<NotificationPanelLocation>('right')
}
public getNotificationLocation(): NotificationPanelLocation {
return this.locationState.value
}
public setNotificationLocation(location: NotificationPanelLocation): void {
this.locationState.value = location
}
public getNotifications(): WebNotification[] {
return this.state.value
}
protected addNotificationToStorage(notification: WebNotification): void {
this.state.value.push(notification)
}
protected removeNotificationFromStorage(id: string | number): void {
const index = this.state.value.findIndex((n) => n.id === id)
if (index > -1) {
this.state.value.splice(index, 1)
}
}
protected removeNotificationFromStorageByIndex(index: number): void {
this.state.value.splice(index, 1)
}
protected clearAllNotificationsFromStorage(): void {
this.state.value.splice(0)
}
}
+14 -10
View File
@@ -1,6 +1,4 @@
import { trackEvent } from '@/helpers/analytics.js' import { defineStore } from 'pinia'
import { get_project, get_version_many } from '@/helpers/cache.js'
import { create_profile_and_install as packInstall } from '@/helpers/pack.js'
import { import {
add_project_from_version, add_project_from_version,
check_installed, check_installed,
@@ -9,9 +7,11 @@ import {
list, list,
remove_project, remove_project,
} from '@/helpers/profile.js' } from '@/helpers/profile.js'
import { injectNotificationManager } from '@modrinth/ui' import { handleError } from '@/store/notifications.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import { create_profile_and_install as packInstall } from '@/helpers/pack.js'
import { trackEvent } from '@/helpers/analytics.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { defineStore } from 'pinia'
export const useInstall = defineStore('installStore', { export const useInstall = defineStore('installStore', {
state: () => ({ state: () => ({
@@ -29,8 +29,8 @@ export const useInstall = defineStore('installStore', {
setIncompatibilityWarningModal(ref) { setIncompatibilityWarningModal(ref) {
this.incompatibilityWarningModal = ref this.incompatibilityWarningModal = ref
}, },
showIncompatibilityWarningModal(instance, project, versions, onInstall) { showIncompatibilityWarningModal(instance, project, versions, selected, onInstall) {
this.incompatibilityWarningModal.show(instance, project, versions, onInstall) this.incompatibilityWarningModal.show(instance, project, versions, selected, onInstall)
}, },
setModInstallModal(ref) { setModInstallModal(ref) {
this.modInstallModal = ref this.modInstallModal = ref
@@ -49,7 +49,6 @@ export const install = async (
callback = () => {}, callback = () => {},
createInstanceCallback = () => {}, createInstanceCallback = () => {},
) => { ) => {
const { handleError } = injectNotificationManager()
const project = await get_project(projectId, 'must_revalidate').catch(handleError) const project = await get_project(projectId, 'must_revalidate').catch(handleError)
if (project.project_type === 'modpack') { if (project.project_type === 'modpack') {
@@ -134,7 +133,13 @@ export const install = async (
callback(version.id) callback(version.id)
} else { } else {
const install = useInstall() const install = useInstall()
install.showIncompatibilityWarningModal(instance, project, projectVersions, callback) install.showIncompatibilityWarningModal(
instance,
project,
projectVersions,
version,
callback,
)
} }
} else { } else {
const versions = (await get_version_many(project.versions).catch(handleError)).sort( const versions = (await get_version_many(project.versions).catch(handleError)).sort(
@@ -161,7 +166,6 @@ export const install = async (
} }
export const installVersionDependencies = async (profile, version) => { export const installVersionDependencies = async (profile, version) => {
const { handleError } = injectNotificationManager()
for (const dep of version.dependencies) { for (const dep of version.dependencies) {
if (dep.dependency_type !== 'required') continue if (dep.dependency_type !== 'required') continue
// disallow fabric api install on quilt // disallow fabric api install on quilt
@@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
export const useNotifications = defineStore('notificationsStore', {
state: () => ({
notificationsWrapper: null,
}),
actions: {
setNotifs(notifs) {
this.notificationsWrapper = notifs
},
addNotification(notif) {
this.notificationsWrapper.addNotification(notif)
},
},
})
export const handleError = (err) => {
const notifs = useNotifications()
notifs.addNotification({
title: 'An error occurred',
text: err.message ?? err,
type: 'error',
})
console.error(err)
}
+5 -4
View File
@@ -1,6 +1,7 @@
import { useBreadcrumbs } from './breadcrumbs'
import { useInstall } from './install'
import { useLoading } from './loading'
import { useTheming } from './theme.ts' import { useTheming } from './theme.ts'
import { useBreadcrumbs } from './breadcrumbs'
import { useLoading } from './loading'
import { useNotifications, handleError } from './notifications'
import { useInstall } from './install'
export { useBreadcrumbs, useInstall, useLoading, useTheming } export { useTheming, useBreadcrumbs, useLoading, useNotifications, handleError, useInstall }
+2 -5
View File
@@ -1,14 +1,11 @@
<template> <template>
<NuxtLayout> <NuxtLayout>
<ModrinthLoadingIndicator /> <ModrinthLoadingIndicator />
<NotificationPanel /> <Notifications />
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NotificationPanel, provideNotificationManager } from "@modrinth/ui";
import { FrontendNotificationManager } from "./providers/frontend-notifications.ts";
import ModrinthLoadingIndicator from "~/components/ui/modrinth-loading-indicator.ts"; import ModrinthLoadingIndicator from "~/components/ui/modrinth-loading-indicator.ts";
import Notifications from "~/components/ui/Notifications.vue";
provideNotificationManager(new FrontendNotificationManager());
</script> </script>
@@ -51,9 +51,7 @@
<script setup> <script setup>
import { PlusIcon, XIcon } from "@modrinth/assets"; import { PlusIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { injectNotificationManager } from "@modrinth/ui";
const { addNotification } = injectNotificationManager();
const router = useNativeRouter(); const router = useNativeRouter();
const name = ref(""); const name = ref("");
@@ -89,6 +87,7 @@ async function create() {
await router.push(`/collection/${result.id}`); await router.push(`/collection/${result.id}`);
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err?.data?.description || err?.message || err, text: err?.data?.description || err?.message || err,
type: "error", type: "error",
@@ -84,11 +84,8 @@
</template> </template>
<script setup> <script setup>
import { PlusIcon, XIcon } from "@modrinth/assets"; import { NewModal, ButtonStyled, DropdownSelect } from "@modrinth/ui";
import { ButtonStyled, DropdownSelect, NewModal } from "@modrinth/ui"; import { XIcon, PlusIcon } from "@modrinth/assets";
import { injectNotificationManager } from "@modrinth/ui";
const { addNotification } = injectNotificationManager();
const router = useRouter(); const router = useRouter();
const app = useNuxtApp(); const app = useNuxtApp();
@@ -183,7 +180,8 @@ async function createProject() {
}, },
}); });
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -319,21 +319,30 @@
</template> </template>
<script setup> <script setup>
import { renderString } from "@modrinth/utils";
import { import {
BellIcon,
CalendarIcon,
CheckCircleIcon,
CheckIcon,
ExternalIcon,
ScaleIcon,
UserPlusIcon, UserPlusIcon,
ScaleIcon,
BellIcon,
CheckCircleIcon,
CalendarIcon,
VersionIcon, VersionIcon,
CheckIcon,
XIcon, XIcon,
ExternalIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { injectNotificationManager } from "@modrinth/ui"; import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
import { getUserLink } from "~/helpers/users.js";
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
import { markAsRead } from "~/helpers/notifications.ts";
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
import Categories from "~/components/ui/search/Categories.vue";
const { addNotification } = injectNotificationManager(); const app = useNuxtApp();
const emit = defineEmits(["update:notifications"]); const emit = defineEmits(["update:notifications"]);
const formatRelativeTime = useRelativeTime(); const formatRelativeTime = useRelativeTime();
const props = defineProps({ const props = defineProps({
@@ -398,7 +407,8 @@ async function read() {
const newNotifs = updateNotifs(props.notifications); const newNotifs = updateNotifs(props.notifications);
emit("update:notifications", newNotifs); emit("update:notifications", newNotifs);
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "Error marking notification as read", title: "Error marking notification as read",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -417,7 +427,8 @@ async function performAction(notification, actionIndex) {
}); });
} }
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -3,9 +3,7 @@
class="vue-notification-group experimental-styles-within" class="vue-notification-group experimental-styles-within"
:class="{ :class="{
'intercom-present': isIntercomPresent, 'intercom-present': isIntercomPresent,
'location-left': notificationLocation === 'left', rightwards: moveNotificationsRight,
'location-right': notificationLocation === 'right',
'has-sidebar': hasSidebar,
}" }"
> >
<transition-group name="notifs"> <transition-group name="notifs">
@@ -55,7 +53,7 @@
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled circular size="small"> <ButtonStyled circular size="small">
<button v-tooltip="`Dismiss`" @click="dismissNotification(index)"> <button v-tooltip="`Dismiss`" @click="notifications.splice(index, 1)">
<XIcon /> <XIcon />
</button> </button>
</ButtonStyled> </ButtonStyled>
@@ -75,117 +73,106 @@
</transition-group> </transition-group>
</div> </div>
</template> </template>
<script setup>
<script setup lang="ts"> import { ButtonStyled } from "@modrinth/ui";
import { import {
XCircleIcon,
CheckCircleIcon, CheckCircleIcon,
CheckIcon, CheckIcon,
CopyIcon,
InfoIcon, InfoIcon,
IssuesIcon, IssuesIcon,
XCircleIcon,
XIcon, XIcon,
} from '@modrinth/assets' CopyIcon,
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' } from "@modrinth/assets";
import { injectNotificationManager, type WebNotification } from '../../providers' const notifications = useNotifications();
import ButtonStyled from '../base/ButtonStyled.vue' const { isVisible: moveNotificationsRight } = useNotificationRightwards();
const notificationManager = injectNotificationManager() const isIntercomPresent = ref(false);
const notifications = computed<WebNotification[]>(() => notificationManager.getNotifications())
const notificationLocation = computed(() => notificationManager.getNotificationLocation())
const isIntercomPresent = ref<boolean>(false) function stopTimer(notif) {
const copied = ref<Record<string, boolean>>({}) clearTimeout(notif.timer);
const stopTimer = (n: WebNotification) => notificationManager.stopNotificationTimer(n)
const setNotificationTimer = (n: WebNotification) => notificationManager.setNotificationTimer(n)
const dismissNotification = (n: number) => notificationManager.removeNotificationByIndex(n)
function createNotifText(notif: WebNotification): string {
return [notif.title, notif.text, notif.errorCode].filter(Boolean).join('\n')
} }
function checkIntercomPresence(): void { const copied = ref({});
isIntercomPresent.value = !!document.querySelector('.intercom-lightweight-app')
}
function copyToClipboard(notif: WebNotification): void { const createNotifText = (notif) => {
const text = createNotifText(notif) let text = "";
if (notif.title) {
text += notif.title;
}
if (notif.text) {
if (text.length > 0) {
text += "\n";
}
text += notif.text;
}
if (notif.errorCode) {
if (text.length > 0) {
text += "\n";
}
text += notif.errorCode;
}
return text;
};
copied.value[text] = true function checkIntercomPresence() {
navigator.clipboard.writeText(text) isIntercomPresent.value = !!document.querySelector(".intercom-lightweight-app");
setTimeout(() => {
const { [text]: _, ...rest } = copied.value
copied.value = rest
}, 2000)
} }
onMounted(() => { onMounted(() => {
checkIntercomPresence() checkIntercomPresence();
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
checkIntercomPresence() checkIntercomPresence();
}) });
observer.observe(document.body, { observer.observe(document.body, {
childList: true, childList: true,
subtree: true, subtree: true,
}) });
onBeforeUnmount(() => { onBeforeUnmount(() => {
observer.disconnect() observer.disconnect();
}) });
}) });
withDefaults( function copyToClipboard(notif) {
defineProps<{ const text = createNotifText(notif);
hasSidebar?: boolean
}>(), copied.value[text] = true;
{ navigator.clipboard.writeText(text);
hasSidebar: false, setTimeout(() => {
}, delete copied.value[text];
) }, 2000);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.vue-notification-group { .vue-notification-group {
position: fixed; position: fixed;
right: 1.5rem;
bottom: 1.5rem; bottom: 1.5rem;
z-index: 200; z-index: 200;
width: 450px; width: 450px;
&.location-right {
right: 1.5rem;
&.has-sidebar {
right: 325px;
}
}
&.location-left {
left: 1.5rem;
}
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
width: calc(100% - 0.75rem * 2); width: calc(100% - 0.75rem * 2);
right: 0.75rem;
bottom: 0.75rem; bottom: 0.75rem;
&.location-right {
right: 0.75rem;
left: auto;
}
&.location-left {
left: 0.75rem;
right: auto;
}
} }
&.intercom-present { &.intercom-present {
bottom: 5rem; bottom: 5rem;
} }
&.rightwards {
right: unset !important;
left: 1.5rem;
@media screen and (max-width: 500px) {
left: 0.75rem;
}
}
.vue-notification-wrapper { .vue-notification-wrapper {
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
@@ -221,12 +208,6 @@ withDefaults(
} }
.notifs-leave-to { .notifs-leave-to {
.location-right & { transform: translateX(100%) scale(0.8);
transform: translateX(100%) scale(0.8);
}
.location-left & {
transform: translateX(-100%) scale(0.8);
}
} }
</style> </style>
@@ -15,7 +15,7 @@
maxlength="64" maxlength="64"
:placeholder="`Enter organization name...`" :placeholder="`Enter organization name...`"
autocomplete="off" autocomplete="off"
@input="updateSlug" @input="updateSlug()"
/> />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -33,7 +33,7 @@
type="text" type="text"
maxlength="64" maxlength="64"
autocomplete="off" autocomplete="off"
@input="setManualSlug" @input="manualSlug = true"
/> />
</div> </div>
</div> </div>
@@ -61,7 +61,7 @@
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled> <ButtonStyled>
<button @click="hide"> <button @click="modal.hide()">
<XIcon aria-hidden="true" /> <XIcon aria-hidden="true" />
Cancel Cancel
</button> </button>
@@ -70,22 +70,20 @@
</div> </div>
</NewModal> </NewModal>
</template> </template>
<script setup>
<script setup lang="ts"> import { XIcon, PlusIcon } from "@modrinth/assets";
import { PlusIcon, XIcon } from "@modrinth/assets"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ButtonStyled, NewModal, injectNotificationManager } from "@modrinth/ui";
import { ref } from "vue";
const router = useNativeRouter(); const router = useNativeRouter();
const { addNotification } = injectNotificationManager();
const name = ref<string>(""); const name = ref("");
const slug = ref<string>(""); const slug = ref("");
const description = ref<string>(""); const description = ref("");
const manualSlug = ref<boolean>(false); const manualSlug = ref(false);
const modal = ref<InstanceType<typeof NewModal>>();
async function createOrganization(): Promise<void> { const modal = ref();
async function createOrganization() {
startLoading(); startLoading();
try { try {
const value = { const value = {
@@ -94,18 +92,19 @@ async function createOrganization(): Promise<void> {
slug: slug.value.trim().replace(/ +/g, ""), slug: slug.value.trim().replace(/ +/g, ""),
}; };
const result: any = await useBaseFetch("organization", { const result = await useBaseFetch("organization", {
method: "POST", method: "POST",
body: JSON.stringify(value), body: JSON.stringify(value),
apiVersion: 3, apiVersion: 3,
}); });
modal.value?.hide(); modal.value.hide();
await router.push(`/organization/${result.slug}`); await router.push(`/organization/${result.slug}`);
} catch (err: any) { } catch (err) {
console.error(err); console.error(err);
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -113,18 +112,13 @@ async function createOrganization(): Promise<void> {
} }
stopLoading(); stopLoading();
} }
function show(event) {
function show(event?: MouseEvent): void {
name.value = ""; name.value = "";
description.value = ""; description.value = "";
modal.value?.show(event); modal.value.show(event);
} }
function hide(): void { function updateSlug() {
modal.value?.hide();
}
function updateSlug(): void {
if (!manualSlug.value) { if (!manualSlug.value) {
slug.value = name.value slug.value = name.value
.trim() .trim()
@@ -135,10 +129,6 @@ function updateSlug(): void {
} }
} }
function setManualSlug(): void {
manualSlug.value = true;
}
defineExpose({ defineExpose({
show, show,
}); });
@@ -1,511 +1,481 @@
<template> <template>
<div v-if="showInvitation" class="universal-card information invited"> <div v-if="showInvitation" class="universal-card information invited my-4">
<h2>Invitation to join project</h2> <h2>{{ getFormattedMessage(messages.invitationTitle) }}</h2>
<p> <p v-if="currentMember?.project_role">
You've been invited be a member of this project with the role of '{{ currentMember.role }}'. {{ formatMessage(messages.invitationWithRole, { role: currentMember.project_role }) }}
</p> </p>
<p v-else>{{ getFormattedMessage(messages.invitationNoRole) }}</p>
<div class="input-group"> <div class="input-group">
<button class="iconified-button brand-button" @click="acceptInvite()"> <ButtonStyled color="brand">
<CheckIcon /> <button class="brand-button" @click="acceptInvite()">
Accept <CheckIcon />
</button> {{ getFormattedMessage(messages.accept) }}
<button class="iconified-button danger-button" @click="declineInvite()"> </button>
<XIcon /> </ButtonStyled>
Decline <ButtonStyled color="red">
</button> <button @click="declineInvite">
<XIcon />
{{ getFormattedMessage(messages.decline) }}
</button>
</ButtonStyled>
</div> </div>
</div> </div>
<div <div
v-if=" v-if="
currentMember && currentMember &&
nags.filter((x) => x.condition).length > 0 && visibleNags.length > 0 &&
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status)) (project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
" "
class="author-actions universal-card mb-4" class="universal-card my-4"
> >
<div class="header__row"> <div class="flex max-w-full flex-wrap items-center gap-x-6 gap-y-4">
<div class="header__title"> <div class="flex flex-auto flex-wrap items-center gap-x-6 gap-y-4">
<h2>Publishing checklist</h2> <h2 class="my-0 mr-auto">{{ getFormattedMessage(messages.publishingChecklist) }}</h2>
<div class="checklist"> <div class="flex flex-row gap-2">
<span class="checklist__title">Progress:</span> <div class="flex items-center gap-1">
<div class="checklist__items"> <AsteriskIcon class="size-4 text-red" />
<div <span class="text-secondary">{{ getFormattedMessage(messages.required) }}</span>
v-for="nag in nags" </div>
:key="`checklist-${nag.id}`" |
v-tooltip="nag.title" <div class="flex items-center gap-1">
:aria-label="nag.title" <TriangleAlertIcon class="size-4 text-orange" />
:class="'circle ' + (!nag.condition ? 'done' : '') + nag.status" <span class="text-secondary">{{ getFormattedMessage(messages.warning) }}</span>
class="circle" </div>
> |
<CheckIcon v-if="!nag.condition" /> <div class="flex items-center gap-1">
<AsteriskIcon v-else-if="nag.status === 'required'" /> <LightBulbIcon class="size-4 text-purple" />
<LightBulbIcon v-else-if="nag.status === 'suggestion'" /> <span class="text-secondary">{{ getFormattedMessage(messages.suggestion) }}</span>
<ScaleIcon v-else-if="nag.status === 'review'" />
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="input-group"> <div class="input-group">
<button <ButtonStyled circular>
:class="{ 'not-collapsed': !collapsed }" <button :class="!collapsed && '[&>svg]:rotate-180'" @click="toggleCollapsed()">
class="square-button" <DropdownIcon class="duration-250 transition-transform ease-in-out" />
@click="toggleCollapsed()" </button>
> </ButtonStyled>
<DropdownIcon />
</button>
</div> </div>
</div> </div>
<div v-if="!collapsed" class="grid-display width-16"> <div v-if="!collapsed" class="grid-display width-16 mt-4">
<div <div v-for="nag in visibleNags" :key="nag.id" class="grid-display__item">
v-for="nag in nags.filter((x) => x.condition && !x.hide)" <span class="flex items-center gap-2 font-semibold">
:key="nag.id" <component
class="grid-display__item" :is="nag.icon || getDefaultIcon(nag.status)"
> v-tooltip="getStatusTooltip(nag.status)"
<span class="label"> :class="[
<AsteriskIcon 'size-4',
v-if="nag.status === 'required'" nag.status === 'required' && 'text-red',
v-tooltip="'Required'" nag.status === 'warning' && 'text-orange',
:class="nag.status" nag.status === 'suggestion' && 'text-purple',
aria-label="Required" ]"
:aria-label="getStatusTooltip(nag.status)"
/> />
<LightBulbIcon {{ getFormattedMessage(nag.title) }}
v-else-if="nag.status === 'suggestion'" </span>
v-tooltip="'Suggestion'" {{ getNagDescription(nag) }}
:class="nag.status"
aria-label="Suggestion"
/>
<ScaleIcon
v-else-if="nag.status === 'review'"
v-tooltip="'Review'"
:class="nag.status"
aria-label="Review"
/>{{ nag.title }}</span
>
{{ nag.description }}
<NuxtLink <NuxtLink
v-if="nag.link" v-if="nag.link && shouldShowLink(nag)"
:class="{ invisible: nag.link.hide }"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${ :to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
nag.link.path nag.link.path
}`" }`"
class="goto-link" class="goto-link"
> >
{{ nag.link.title }} {{ getFormattedMessage(nag.link.title) }}
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" /> <ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
</NuxtLink> </NuxtLink>
<button <ButtonStyled
v-else-if="nag.action" v-if="nag.status === 'special-submit-action' && nag.id === 'submit-for-review'"
:disabled="nag.action.disabled()" color="orange"
class="btn btn-orange" @click="submitForReview"
@click="nag.action.onClick"
> >
<SendIcon /> <button
{{ nag.action.title }} :disabled="!canSubmitForReview"
</button> v-tooltip="
!canSubmitForReview ? getFormattedMessage(messages.submitChecklistTooltip) : undefined
"
>
<SendIcon />
{{ getFormattedMessage(messages.submitForReview) }}
</button>
</ButtonStyled>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { import {
AsteriskIcon,
CheckIcon,
ChevronRightIcon, ChevronRightIcon,
DropdownIcon, CheckIcon,
LightBulbIcon,
ScaleIcon,
SendIcon,
XIcon, XIcon,
AsteriskIcon,
LightBulbIcon,
TriangleAlertIcon,
DropdownIcon,
SendIcon,
ScaleIcon,
InfoIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { injectNotificationManager } from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js"; import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
import { nags } from "@modrinth/moderation";
import { ButtonStyled } from "@modrinth/ui";
import { useVIntl, defineMessages, type MessageDescriptor } from "@vintl/vintl";
import type { Nag, NagContext, NagStatus } from "@modrinth/moderation";
import type { Project, User, Version } from "@modrinth/utils";
import type { Component } from "vue";
const props = defineProps({ interface Tags {
project: { rejectedStatuses: string[];
type: Object, }
required: true,
interface Auth {
user: {
id: string;
};
}
interface Member {
accepted?: boolean;
project_role?: string;
user?: Partial<User>;
}
interface Props {
project: Project;
versions?: Version[];
currentMember?: Member | null;
allMembers?: Member[] | null;
isSettings?: boolean;
collapsed?: boolean;
routeName?: string;
auth: Auth;
tags: Tags;
setProcessing?: (processing: boolean) => void;
toggleCollapsed?: () => void;
updateMembers?: () => void | Promise<void>;
}
const messages = defineMessages({
invitationTitle: {
id: "project-member-header.invitation-title",
defaultMessage: "Invitation to join project",
}, },
versions: { invitationWithRole: {
type: Array, id: "project-member-header.invitation-with-role",
default() { defaultMessage: "You've been invited be a member of this project with the role of '{role}'.",
return [];
},
}, },
currentMember: { invitationNoRole: {
type: Object, id: "project-member-header.invitation-no-role",
default: null, defaultMessage:
"You've been invited to join this project. Please accept or decline the invitation.",
}, },
allMembers: { accept: {
type: Object, id: "project-member-header.accept",
default: null, defaultMessage: "Accept",
}, },
isSettings: { decline: {
type: Boolean, id: "project-member-header.decline",
default: false, defaultMessage: "Decline",
}, },
collapsed: { publishingChecklist: {
type: Boolean, id: "project-member-header.publishing-checklist",
default: false, defaultMessage: "Publishing checklist",
}, },
routeName: { submitForReview: {
type: String, id: "project-member-header.submit-for-review",
default: "", defaultMessage: "Submit for review",
}, },
auth: { submitForReviewDesc: {
type: Object, id: "project-member-header.submit-for-review-desc",
required: true, defaultMessage:
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
}, },
tags: { resubmitForReview: {
type: Object, id: "project-member-header.resubmit-for-review",
required: true, defaultMessage: "Resubmit for review",
}, },
setProcessing: { resubmitForReviewDesc: {
type: Function, id: "project-member-header.resubmit-for-review-desc",
default() { defaultMessage:
return () => { "Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
const { addNotification } = injectNotificationManager();
addNotification({
title: "An error occurred",
text: "setProcessing function not found",
type: "error",
});
};
},
}, },
toggleCollapsed: { showKey: {
type: Function, id: "project-member-header.show-key",
default() { defaultMessage: "Toggle key",
return () => {
const { addNotification } = injectNotificationManager();
addNotification({
title: "An error occurred",
text: "toggleCollapsed function not found",
type: "error",
});
};
},
}, },
updateMembers: { keyTitle: {
type: Function, id: "project-member-header.key-title",
default() { defaultMessage: "Status Key",
return () => { },
const { addNotification } = injectNotificationManager(); action: {
addNotification({ id: "project-member-header.action",
title: "An error occurred", defaultMessage: "Action",
text: "updateMembers function not found", },
type: "error", visitModerationPage: {
}); id: "project-member-header.visit-moderation-page",
}; defaultMessage: "Visit moderation page",
}, },
submitChecklistTooltip: {
id: "project-member-header.submit-checklist-tooltip",
defaultMessage: "You must complete the required steps in the publishing checklist!",
},
successJoin: {
id: "project-member-header.success-join",
defaultMessage: "You have joined the project team",
},
errorJoin: {
id: "project-member-header.error-join",
defaultMessage: "Failed to accept team invitation",
},
successDecline: {
id: "project-member-header.success-decline",
defaultMessage: "You have declined the team invitation",
},
errorDecline: {
id: "project-member-header.error-decline",
defaultMessage: "Failed to decline team invitation",
},
success: {
id: "project-member-header.success",
defaultMessage: "Success",
},
error: {
id: "project-member-header.error",
defaultMessage: "Error",
},
required: {
id: "project-member-header.required",
defaultMessage: "Required",
},
warning: {
id: "project-member-header.warning",
defaultMessage: "Warning",
},
suggestion: {
id: "project-member-header.suggestion",
defaultMessage: "Suggestion",
}, },
}); });
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured)); const { formatMessage } = useVIntl();
const nags = computed(() => [ function getNagDescription(nag: Nag): string {
{ if (typeof nag.description === "function") {
condition: props.versions.length < 1, return nag.description(nagContext.value);
title: "Upload a version", }
id: "upload-version", return formatMessage(nag.description);
description: "At least one version is required for a project to be submitted for review.", }
status: "required",
link: {
path: "versions",
title: "Visit versions page",
hide: props.routeName === "type-id-versions",
},
},
{
condition:
props.project.body === "" || props.project.body.startsWith("# Placeholder description"),
title: "Add a description",
id: "add-description",
description:
"A description that clearly describes the project's purpose and function is required.",
status: "required",
link: {
path: "settings/description",
title: "Visit description settings",
hide: props.routeName === "type-id-settings-description",
},
},
{
condition: !props.project.icon_url,
title: "Add an icon",
id: "add-icon",
description:
"Your project should have a nice-looking icon to uniquely identify your project at a glance.",
status: "suggestion",
link: {
path: "settings",
title: "Visit general settings",
hide: props.routeName === "type-id-settings",
},
},
{
condition: props.project.gallery.length === 0 || !featuredGalleryImage,
title: "Feature a gallery image",
id: "feature-gallery-image",
description: "Featured gallery images may be the first impression of many users.",
status: "suggestion",
link: {
path: "gallery",
title: "Visit gallery page",
hide: props.routeName === "type-id-gallery",
},
},
{
hide: props.project.versions.length === 0,
condition: props.project.categories.length < 1,
title: "Select tags",
id: "select-tags",
description: "Select all tags that apply to your project.",
status: "suggestion",
link: {
path: "settings/tags",
title: "Visit tag settings",
hide: props.routeName === "type-id-settings-tags",
},
},
{
condition: !(
props.project.issues_url ||
props.project.source_url ||
props.project.wiki_url ||
props.project.discord_url ||
props.project.donation_urls.length > 0
),
title: "Add external links",
id: "add-links",
description:
"Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.",
status: "suggestion",
link: {
path: "settings/links",
title: "Visit links settings",
hide: props.routeName === "type-id-settings-links",
},
},
{
hide:
props.project.versions.length === 0 ||
props.project.project_type === "resourcepack" ||
props.project.project_type === "plugin" ||
props.project.project_type === "shader" ||
props.project.project_type === "datapack",
condition:
props.project.client_side === "unknown" ||
props.project.server_side === "unknown" ||
(props.project.client_side === "unsupported" && props.project.server_side === "unsupported"),
title: "Select supported environments",
id: "select-environments",
description: `Select if the ${formatProjectType(
props.project.project_type,
).toLowerCase()} functions on the client-side and/or server-side.`,
status: "required",
link: {
path: "settings",
title: "Visit general settings",
hide: props.routeName === "type-id-settings",
},
},
{
condition: props.project.license.id === "LicenseRef-Unknown",
title: "Select license",
id: "select-license",
description: `Select the license your ${formatProjectType(
props.project.project_type,
).toLowerCase()} is distributed under.`,
status: "required",
link: {
path: "settings/license",
title: "Visit license settings",
hide: props.routeName === "type-id-settings-license",
},
},
{
condition: props.project.status === "draft",
title: "Submit for review",
id: "submit-for-review",
description:
"Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.",
status: "review",
link: null,
action: {
onClick: submitForReview,
title: "Submit for review",
disabled: () => nags.value.filter((x) => x.condition && x.status === "required").length > 0,
},
},
{
hide: props.project.stats === "draft",
condition: props.tags.rejectedStatuses.includes(props.project.status),
title: "Resubmit for review",
id: "resubmit-for-review",
description: `Your project has been ${props.project.status} by
Modrinth's staff. In most cases, you can resubmit for review after
addressing the staff's message.`,
status: "review",
link: {
path: "moderation",
title: "Visit moderation page",
hide: props.routeName === "type-id-moderation",
},
},
]);
const showInvitation = computed(() => { function getFormattedMessage(message: string | MessageDescriptor): string {
if (typeof message === "string") {
return message;
}
return formatMessage(message);
}
const props = withDefaults(defineProps<Props>(), {
versions: () => [],
currentMember: null,
allMembers: null,
isSettings: false,
collapsed: false,
routeName: "",
});
const emit = defineEmits<{
toggleCollapsed: [];
updateMembers: [];
setProcessing: [processing: boolean];
}>();
const nagContext = computed<NagContext>(() => ({
project: props.project,
versions: props.versions,
currentMember: props.currentMember as User,
currentRoute: props.routeName,
tags: props.tags,
submitProject: submitForReview,
}));
const showKey = ref(false);
function toggleKey(): void {
showKey.value = !showKey.value;
}
const canSubmitForReview = computed(() => {
return (
applicableNags.value.filter((nag) => nag.status === "required" && !isNagComplete(nag))
.length === 0
);
});
async function submitForReview() {
if (canSubmitForReview) {
await setProcessing(true);
}
}
const applicableNags = computed<Nag[]>(() => {
return nags.filter((nag) => {
return nag.shouldShow(nagContext.value);
});
});
function isNagComplete(nag: Nag): boolean {
const context = nagContext.value;
return !nag.shouldShow(context);
}
const visibleNags = computed<Nag[]>(() => {
const finalNags = applicableNags.value.filter((nag) => !isNagComplete(nag));
if (props.project.status === "draft") {
finalNags.push({
id: "submit-for-review",
title: messages.submitForReview,
description: () => formatMessage(messages.submitForReviewDesc),
status: "special-submit-action",
shouldShow: (ctx) => ctx.project.status === "draft",
});
}
if (props.tags.rejectedStatuses.includes(props.project.status)) {
finalNags.push({
id: "resubmit-for-review",
title: messages.resubmitForReview,
description: (ctx) =>
formatMessage(messages.resubmitForReviewDesc, { status: ctx.project.status }),
status: "special-submit-action",
shouldShow: (ctx) => ctx.tags.rejectedStatuses.includes(ctx.project.status),
link: {
path: "moderation",
title: messages.visitModerationPage,
shouldShow: () => props.routeName !== "type-id-moderation",
},
});
}
finalNags.sort((a, b) => {
const statusOrder = { required: 0, warning: 1, suggestion: 2, "special-submit-action": 3 };
return statusOrder[a.status] - statusOrder[b.status];
});
return finalNags;
});
function shouldShowLink(nag: Nag): boolean {
return nag.link?.shouldShow ? nag.link.shouldShow(nagContext.value) : false;
}
function getDefaultIcon(status: NagStatus): Component {
switch (status) {
case "required":
return AsteriskIcon;
case "warning":
return TriangleAlertIcon;
case "suggestion":
return LightBulbIcon;
case "special-submit-action":
return ScaleIcon;
default:
return AsteriskIcon;
}
}
function getStatusTooltip(status: NagStatus): string {
switch (status) {
case "required":
return formatMessage(messages.required);
case "warning":
return formatMessage(messages.warning);
case "suggestion":
return formatMessage(messages.suggestion);
default:
return formatMessage(messages.required);
}
}
const showInvitation = computed<boolean>(() => {
if (props.allMembers && props.auth) { if (props.allMembers && props.auth) {
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id); const member = props.allMembers.find((x) => x?.user?.id === props.auth.user.id);
return member && !member.accepted; return !!member && !member.accepted;
} }
return false; return false;
}); });
const acceptInvite = () => { function toggleCollapsed(): void {
acceptTeamInvite(props.project.team); if (props.toggleCollapsed) {
props.updateMembers(); props.toggleCollapsed();
}; } else {
emit("toggleCollapsed");
const declineInvite = () => {
removeTeamMember(props.project.team, props.auth.user.id);
props.updateMembers();
};
const submitForReview = async () => {
if (
!props.acknowledgedMessage ||
nags.value.filter((x) => x.condition && x.status === "required").length === 0
) {
await props.setProcessing();
} }
}; }
async function updateMembers(): Promise<void> {
if (props.updateMembers) {
await props.updateMembers();
} else {
emit("updateMembers");
}
}
function setProcessing(processing: boolean): void {
if (props.setProcessing) {
props.setProcessing(processing);
} else {
emit("setProcessing", processing);
}
}
async function acceptInvite(): Promise<void> {
try {
setProcessing(true);
await acceptTeamInvite(props.project.team);
await updateMembers();
addNotification({
group: "main",
title: formatMessage(messages.success),
text: formatMessage(messages.successJoin),
type: "success",
});
} catch (error) {
addNotification({
group: "main",
title: formatMessage(messages.error),
text: formatMessage(messages.errorJoin),
type: "error",
});
} finally {
setProcessing(false);
}
}
async function declineInvite(): Promise<void> {
try {
setProcessing(true);
await removeTeamMember(props.project.team, props.auth.user.id);
await updateMembers();
addNotification({
group: "main",
title: formatMessage(messages.success),
text: formatMessage(messages.successDecline),
type: "success",
});
} catch (error) {
addNotification({
group: "main",
title: formatMessage(messages.error),
text: formatMessage(messages.errorDecline),
type: "error",
});
} finally {
setProcessing(false);
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.invited { .duration-250 {
} transition-duration: 250ms;
.author-actions {
margin-top: var(--spacing-card-md);
&:empty {
display: none;
}
.invisible {
visibility: hidden;
}
.header__row {
align-items: center;
column-gap: var(--spacing-card-lg);
row-gap: var(--spacing-card-md);
max-width: 100%;
.header__title {
display: flex;
flex-wrap: wrap;
align-items: center;
column-gap: var(--spacing-card-lg);
row-gap: var(--spacing-card-md);
flex-basis: min-content;
h2 {
margin: 0 auto 0 0;
}
}
button {
svg {
transition: transform 0.25s ease-in-out;
}
&.not-collapsed svg {
transform: rotate(180deg);
}
}
}
.grid-display__item .label {
display: flex;
gap: var(--spacing-card-xs);
align-items: center;
.required {
color: var(--color-red);
}
.suggestion {
color: var(--color-purple);
}
.review {
color: var(--color-orange);
}
}
.checklist {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-card-xs);
width: fit-content;
flex-wrap: wrap;
max-width: 100%;
.checklist__title {
font-weight: bold;
margin-right: var(--spacing-card-xs);
color: var(--color-text-dark);
}
.checklist__items {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-card-xs);
width: fit-content;
max-width: 100%;
}
.circle {
--circle-size: 2rem;
--background-color: var(--color-bg);
--content-color: var(--color-gray);
width: var(--circle-size);
height: var(--circle-size);
border-radius: 50%;
background-color: var(--background-color);
display: flex;
justify-content: center;
align-items: center;
svg {
color: var(--content-color);
width: calc(var(--circle-size) / 2);
height: calc(var(--circle-size) / 2);
}
&.required {
--content-color: var(--color-red);
}
&.suggestion {
--content-color: var(--color-purple);
}
&.review {
--content-color: var(--color-orange);
}
&.done {
--background-color: var(--color-green);
--content-color: var(--color-brand-inverted);
}
}
}
} }
</style> </style>
@@ -118,25 +118,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import dayjs from "dayjs";
ClipboardCopyIcon,
EllipsisVerticalIcon,
EyeIcon,
LinkIcon,
OrganizationIcon,
} from "@modrinth/assets";
import type { ExtendedDelphiReport } from "@modrinth/moderation";
import { import {
Avatar, Avatar,
ButtonStyled,
injectNotificationManager,
OverflowMenu,
useRelativeTime, useRelativeTime,
OverflowMenu,
type OverflowMenuOption, type OverflowMenuOption,
ButtonStyled,
} from "@modrinth/ui"; } from "@modrinth/ui";
import dayjs from "dayjs"; import {
EllipsisVerticalIcon,
const { addNotification } = injectNotificationManager(); OrganizationIcon,
EyeIcon,
ClipboardCopyIcon,
LinkIcon,
} from "@modrinth/assets";
import type { ExtendedDelphiReport } from "@modrinth/moderation";
const props = defineProps<{ const props = defineProps<{
report: ExtendedDelphiReport; report: ExtendedDelphiReport;
@@ -135,31 +135,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import {
ClipboardCopyIcon, Avatar,
useRelativeTime,
OverflowMenu,
type OverflowMenuOption,
CollapsibleRegion,
ButtonStyled,
} from "@modrinth/ui";
import {
EllipsisVerticalIcon, EllipsisVerticalIcon,
EyeIcon,
LinkIcon,
OrganizationIcon, OrganizationIcon,
EyeIcon,
ClipboardCopyIcon,
LinkIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
type ExtendedReport, type ExtendedReport,
reportQuickReplies, reportQuickReplies,
type ReportQuickReply, type ReportQuickReply,
} from "@modrinth/moderation"; } from "@modrinth/moderation";
import {
Avatar,
ButtonStyled,
CollapsibleRegion,
injectNotificationManager,
OverflowMenu,
type OverflowMenuOption,
useRelativeTime,
} from "@modrinth/ui";
import ChevronDownIcon from "../servers/icons/ChevronDownIcon.vue"; import ChevronDownIcon from "../servers/icons/ChevronDownIcon.vue";
import ReportThread from "../thread/ReportThread.vue"; import ReportThread from "../thread/ReportThread.vue";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
report: ExtendedReport; report: ExtendedReport;
}>(); }>();
@@ -313,66 +313,65 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
LeftArrowIcon,
RightArrowIcon,
DropdownIcon,
XIcon,
ScaleIcon,
ListBulletedIcon,
FileTextIcon,
BrushCleaningIcon, BrushCleaningIcon,
CheckIcon, CheckIcon,
DropdownIcon,
EyeOffIcon,
FileTextIcon,
KeyboardIcon, KeyboardIcon,
LeftArrowIcon, EyeOffIcon,
ListBulletedIcon,
RightArrowIcon,
ScaleIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
type Action,
type ButtonAction,
type ConditionalButtonAction,
type DropdownAction,
type MultiSelectChipsAction,
type Stage,
type ToggleAction,
checklist, checklist,
deserializeActionStates,
expandVariables,
finalPermissionMessages,
findMatchingVariant,
flattenProjectVariables,
getActionIdForStage, getActionIdForStage,
getActionMessage,
getVisibleInputs,
handleKeybind,
initializeActionState, initializeActionState,
kebabToTitleCase, getActionMessage,
keybinds, findMatchingVariant,
processMessage, processMessage,
getVisibleInputs,
serializeActionStates, serializeActionStates,
deserializeActionStates,
kebabToTitleCase,
flattenProjectVariables,
expandVariables,
handleKeybind,
keybinds,
} from "@modrinth/moderation"; } from "@modrinth/moderation";
import { import {
ButtonStyled, ButtonStyled,
Checkbox,
Collapsible, Collapsible,
DropdownSelect,
MarkdownEditor,
OverflowMenu, OverflowMenu,
type OverflowMenuOption, type OverflowMenuOption,
injectNotificationManager, Checkbox,
DropdownSelect,
MarkdownEditor,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { import {
type ModerationJudgements,
type ModerationModpackItem,
type Project, type Project,
renderHighlightedString, renderHighlightedString,
type ModerationJudgements,
type ModerationModpackItem,
type ProjectStatus, type ProjectStatus,
} from "@modrinth/utils"; } from "@modrinth/utils";
import { computedAsync, useLocalStorage } from "@vueuse/core"; import { computedAsync, useLocalStorage } from "@vueuse/core";
import {
type Action,
type MultiSelectChipsAction,
type DropdownAction,
type ButtonAction,
type ToggleAction,
type ConditionalButtonAction,
type Stage,
finalPermissionMessages,
} from "@modrinth/moderation";
import * as prettier from "prettier"; import * as prettier from "prettier";
import { useModerationStore } from "~/store/moderation.ts";
import KeybindsModal from "./ChecklistKeybindsModal.vue";
import ModpackPermissionsFlow from "./ModpackPermissionsFlow.vue"; import ModpackPermissionsFlow from "./ModpackPermissionsFlow.vue";
import KeybindsModal from "./ChecklistKeybindsModal.vue";
const { addNotification } = injectNotificationManager(); import { useModerationStore } from "~/store/moderation.ts";
const keybindsModal = ref<InstanceType<typeof KeybindsModal>>(); const keybindsModal = ref<InstanceType<typeof KeybindsModal>>();
@@ -42,14 +42,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, computed } from "vue";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets"; import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, injectNotificationManager, NewModal } from "@modrinth/ui";
import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils"; import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils";
import { computed, nextTick, ref } from "vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;
}>(); }>();
@@ -45,14 +45,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { IssuesIcon, SaveIcon, SpinnerIcon, XIcon } from "@modrinth/assets"; import { ref, nextTick, computed } from "vue";
import { ButtonStyled, injectNotificationManager, NewModal } from "@modrinth/ui"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
import type { Backup } from "@modrinth/utils"; import type { Backup } from "@modrinth/utils";
import { computed, nextTick, ref } from "vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;
}>(); }>();
@@ -17,14 +17,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ConfirmModal, injectNotificationManager, NewModal } from "@modrinth/ui";
import type { Backup } from "@modrinth/utils";
import { ref } from "vue"; import { ref } from "vue";
import { ConfirmModal, NewModal } from "@modrinth/ui";
import type { Backup } from "@modrinth/utils";
import BackupItem from "~/components/ui/servers/BackupItem.vue"; import BackupItem from "~/components/ui/servers/BackupItem.vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;
}>(); }>();
@@ -56,13 +56,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SaveIcon, XIcon } from "@modrinth/assets"; import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ButtonStyled, injectNotificationManager, NewModal } from "@modrinth/ui"; import { XIcon, SaveIcon } from "@modrinth/assets";
import { computed, ref } from "vue"; import { ref, computed } from "vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;
}>(); }>();
@@ -112,6 +110,7 @@ const fetchSettings = async () => {
} catch (error) { } catch (error) {
console.error("Error fetching backup settings:", error); console.error("Error fetching backup settings:", error);
addNotification({ addNotification({
group: "server",
title: "Error", title: "Error",
text: "Failed to load backup settings", text: "Failed to load backup settings",
type: "error", type: "error",
@@ -136,6 +135,7 @@ const saveSettings = async () => {
}; };
addNotification({ addNotification({
group: "server",
title: "Success", title: "Success",
text: "Backup settings updated successfully", text: "Backup settings updated successfully",
type: "success", type: "success",
@@ -145,6 +145,7 @@ const saveSettings = async () => {
} catch (error) { } catch (error) {
console.error("Error saving backup settings:", error); console.error("Error saving backup settings:", error);
addNotification({ addNotification({
group: "server",
title: "Error", title: "Error",
text: "Failed to save backup settings", text: "Failed to save backup settings",
type: "error", type: "error",
@@ -101,13 +101,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CheckCircleIcon, FolderOpenIcon, XCircleIcon } from "@modrinth/assets"; import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
import { ButtonStyled, injectNotificationManager } from "@modrinth/ui"; import { ButtonStyled } from "@modrinth/ui";
import { computed, nextTick, ref, watch } from "vue"; import { ref, computed, watch, nextTick } from "vue";
import { FSModule } from "~/composables/servers/modules/fs.ts"; import { FSModule } from "~/composables/servers/modules/fs.ts";
const { addNotification } = injectNotificationManager();
interface UploadItem { interface UploadItem {
file: File; file: File;
progress: number; progress: number;
@@ -284,6 +282,7 @@ const uploadFile = async (file: File) => {
if (error instanceof Error && error.message !== "Upload cancelled") { if (error instanceof Error && error.message !== "Upload cancelled") {
addNotification({ addNotification({
group: "files",
title: "Upload failed", title: "Upload failed",
text: `Failed to upload ${file.name}`, text: `Failed to upload ${file.name}`,
type: "error", type: "error",
@@ -67,13 +67,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { DownloadIcon, XIcon } from "@modrinth/assets"; import { DownloadIcon, XIcon } from "@modrinth/assets";
import { ButtonStyled, injectNotificationManager, NewModal } from "@modrinth/ui";
import { ModrinthServersFetchError } from "@modrinth/utils"; import { ModrinthServersFetchError } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
server: ModrinthServer; server: ModrinthServer;
project: any; project: any;
@@ -114,12 +112,14 @@ const handleReinstall = async () => {
} catch (error) { } catch (error) {
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) { if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
addNotification({ addNotification({
group: "server",
title: "Cannot reinstall server", title: "Cannot reinstall server",
text: "You are being rate limited. Please try again later.", text: "You are being rate limited. Please try again later.",
type: "error", type: "error",
}); });
} else { } else {
addNotification({ addNotification({
group: "server",
title: "Reinstall Failed", title: "Reinstall Failed",
text: "An unexpected error occurred while reinstalling. Please try again later.", text: "An unexpected error occurred while reinstalling. Please try again later.",
type: "error", type: "error",
@@ -144,20 +144,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
import { import {
ArrowBigRightDashIcon,
RightArrowIcon,
ServerIcon,
UploadIcon, UploadIcon,
RightArrowIcon,
XIcon, XIcon,
ServerIcon,
ArrowBigRightDashIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { BackupWarning, ButtonStyled, injectNotificationManager, NewModal } from "@modrinth/ui";
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils"; import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue"; import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import type { ModrinthServer } from "~/composables/servers/modrinth-servers";
const { addNotification } = injectNotificationManager();
const handleBeforeUnload = (event: BeforeUnloadEvent) => { const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (isLoading.value) { if (isLoading.value) {
@@ -252,6 +250,7 @@ const handleReinstall = async () => {
if (!mrpackFile.value) { if (!mrpackFile.value) {
addNotification({ addNotification({
group: "server",
title: "No file selected", title: "No file selected",
text: "Choose a .mrpack file before installing.", text: "Choose a .mrpack file before installing.",
type: "error", type: "error",
@@ -302,12 +301,14 @@ const handleReinstall = async () => {
} catch (error) { } catch (error) {
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) { if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
addNotification({ addNotification({
group: "server",
title: "Cannot upload and install modpack to server", title: "Cannot upload and install modpack to server",
text: "You are being rate limited. Please try again later.", text: "You are being rate limited. Please try again later.",
type: "error", type: "error",
}); });
} else { } else {
addNotification({ addNotification({
group: "server",
title: "Modpack upload and install failed", title: "Modpack upload and install failed",
text: "An unexpected error occurred while uploading/installing. Please try again later.", text: "An unexpected error occurred while uploading/installing. Please try again later.",
type: "error", type: "error",
@@ -197,20 +197,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets"; import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
import {
BackupWarning,
ButtonStyled,
injectNotificationManager,
NewModal,
Toggle,
} from "@modrinth/ui";
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
import { $fetch } from "ofetch"; import { $fetch } from "ofetch";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts"; import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue"; import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
interface LoaderVersion { interface LoaderVersion {
@@ -482,12 +475,14 @@ const handleReinstall = async () => {
} catch (error) { } catch (error) {
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) { if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
addNotification({ addNotification({
group: "server",
title: "Cannot reinstall server", title: "Cannot reinstall server",
text: "You are being rate limited. Please try again later.", text: "You are being rate limited. Please try again later.",
type: "error", type: "error",
}); });
} else { } else {
addNotification({ addNotification({
group: "server",
title: "Reinstall Failed", title: "Reinstall Failed",
text: "An unexpected error occurred while reinstalling. Please try again later.", text: "An unexpected error occurred while reinstalling. Please try again later.",
type: "error", type: "error",
@@ -20,11 +20,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { LinkIcon } from "@modrinth/assets"; import { LinkIcon } from "@modrinth/assets";
import { injectNotificationManager } from "@modrinth/ui";
import { useStorage } from "@vueuse/core"; import { useStorage } from "@vueuse/core";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
subdomain: string; subdomain: string;
noSeparator?: boolean; noSeparator?: boolean;
@@ -33,6 +30,7 @@ const props = defineProps<{
const copySubdomain = () => { const copySubdomain = () => {
navigator.clipboard.writeText(props.subdomain + ".modrinth.gg"); navigator.clipboard.writeText(props.subdomain + ".modrinth.gg");
addNotification({ addNotification({
group: "servers",
title: "Custom URL copied", title: "Custom URL copied",
text: "Your server's URL has been copied to your clipboard.", text: "Your server's URL has been copied to your clipboard.",
type: "success", type: "success",
@@ -1,18 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modrinth/ui";
import { PlusIcon, XIcon } from "@modrinth/assets"; import { PlusIcon, XIcon } from "@modrinth/assets";
import { import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
Accordion,
ButtonStyled,
injectNotificationManager,
NewModal,
ServerNotice,
TagItem,
} from "@modrinth/ui";
import { type ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { ref } from "vue"; import { ref } from "vue";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts"; import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
const { addNotification } = injectNotificationManager(); const app = useNuxtApp() as unknown as { $notify: any };
const modal = ref<InstanceType<typeof NewModal>>(); const modal = ref<InstanceType<typeof NewModal>>();
@@ -46,14 +39,16 @@ async function assign(server: boolean = true) {
method: "PUT", method: "PUT",
}, },
).catch((err) => { ).catch((err) => {
addNotification({ app.$notify({
group: "main",
title: "Error assigning notice", title: "Error assigning notice",
text: err, text: err,
type: "error", type: "error",
}); });
}); });
} else { } else {
addNotification({ app.$notify({
group: "main",
title: "Error assigning notice", title: "Error assigning notice",
text: "No server or node specified", text: "No server or node specified",
type: "error", type: "error",
@@ -69,7 +64,8 @@ async function unassignDetect() {
const node = assignedNodes.value.some((assigned) => assigned.id === input); const node = assignedNodes.value.some((assigned) => assigned.id === input);
if (!server && !node) { if (!server && !node) {
addNotification({ app.$notify({
group: "main",
title: "Error unassigning notice", title: "Error unassigning notice",
text: "ID is not an assigned server or node", text: "ID is not an assigned server or node",
type: "error", type: "error",
@@ -88,7 +84,8 @@ async function unassign(id: string, server: boolean = true) {
method: "PUT", method: "PUT",
}, },
).catch((err) => { ).catch((err) => {
addNotification({ app.$notify({
group: "main",
title: "Error unassigning notice", title: "Error unassigning notice",
text: err, text: err,
type: "error", type: "error",
@@ -251,25 +251,23 @@
</template> </template>
<script setup> <script setup>
import { CopyCode, OverflowMenu, MarkdownEditor } from "@modrinth/ui";
import { import {
CheckCircleIcon,
CheckIcon,
DropdownIcon, DropdownIcon,
EyeOffIcon,
ReplyIcon, ReplyIcon,
ScaleIcon,
SendIcon, SendIcon,
CheckCircleIcon,
XIcon, XIcon,
EyeOffIcon,
CheckIcon,
ScaleIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { CopyCode, MarkdownEditor, OverflowMenu, injectNotificationManager } from "@modrinth/ui";
import Checkbox from "~/components/ui/Checkbox.vue";
import Modal from "~/components/ui/Modal.vue";
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
import { useImageUpload } from "~/composables/image-upload.ts"; import { useImageUpload } from "~/composables/image-upload.ts";
import { isApproved, isRejected } from "~/helpers/projects.js"; import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
import { isStaff } from "~/helpers/users.js"; import { isStaff } from "~/helpers/users.js";
import { isApproved, isRejected } from "~/helpers/projects.js";
const { addNotification } = injectNotificationManager(); import Modal from "~/components/ui/Modal.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
const props = defineProps({ const props = defineProps({
thread: { thread: {
@@ -390,7 +388,8 @@ async function sendReply(status = null, privateMessage = false) {
props.setStatus(status); props.setStatus(status);
} }
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "Error sending message", title: "Error sending message",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -412,7 +411,8 @@ async function closeReport(reply) {
}); });
await updateThreadLocal(); await updateThreadLocal();
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "Error closing report", title: "Error closing report",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -430,7 +430,8 @@ async function reopenReport() {
}); });
await updateThreadLocal(); await updateThreadLocal();
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "Error reopening report", title: "Error reopening report",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -110,15 +110,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CheckCircleIcon, ReplyIcon, ScaleIcon, SendIcon } from "@modrinth/assets"; import { CopyCode, MarkdownEditor, ButtonStyled } from "@modrinth/ui";
import { ButtonStyled, CopyCode, injectNotificationManager, MarkdownEditor } from "@modrinth/ui"; import { ReplyIcon, SendIcon, CheckCircleIcon, ScaleIcon } from "@modrinth/assets";
import type { Report, Thread, ThreadMessage as TypeThreadMessage, User } from "@modrinth/utils"; import type { Thread, Report, User, ThreadMessage as TypeThreadMessage } from "@modrinth/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import ThreadMessage from "./ThreadMessage.vue";
import { useImageUpload } from "~/composables/image-upload.ts"; import { useImageUpload } from "~/composables/image-upload.ts";
import { isStaff } from "~/helpers/users.js"; import { isStaff } from "~/helpers/users.js";
import ThreadMessage from "./ThreadMessage.vue";
const { addNotification } = injectNotificationManager();
const props = defineProps<{ const props = defineProps<{
thread: Thread; thread: Thread;
+3 -4
View File
@@ -1,5 +1,3 @@
import { injectNotificationManager } from "@modrinth/ui";
export const useAuth = async (oldToken = null) => { export const useAuth = async (oldToken = null) => {
const auth = useState("auth", () => ({ const auth = useState("auth", () => ({
user: null, user: null,
@@ -130,8 +128,9 @@ export const removeAuthProvider = async (provider) => {
}); });
await useAuth(auth.value.token); await useAuth(auth.value.token);
} catch (err) { } catch (err) {
const { addNotification } = injectNotificationManager(); const data = useNuxtApp();
addNotification({ data.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data.description, text: err.data.description,
type: "error", type: "error",
+38
View File
@@ -0,0 +1,38 @@
export const useNotifications = () => useState("notifications", () => []);
export const addNotification = (notification) => {
const notifications = useNotifications();
const existingNotif = notifications.value.find(
(x) =>
x.text === notification.text &&
x.title === notification.title &&
x.type === notification.type,
);
if (existingNotif) {
setNotificationTimer(existingNotif);
existingNotif.count++;
return;
}
notification.id = new Date();
notification.count = 1;
setNotificationTimer(notification);
notifications.value.push(notification);
};
export const setNotificationTimer = (notification) => {
if (!notification) return;
const notifications = useNotifications();
if (notification.timer) {
clearTimeout(notification.timer);
}
notification.timer = setTimeout(() => {
notifications.value.splice(notifications.value.indexOf(notification), 1);
}, 30000);
};
@@ -1,20 +1,18 @@
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
import { ModrinthServerError } from "@modrinth/utils"; import { ModrinthServerError } from "@modrinth/utils";
import { injectNotificationManager } from "@modrinth/ui"; import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
import { useServersFetch } from "./servers-fetch.ts"; import { useServersFetch } from "./servers-fetch.ts";
import { import {
BackupsModule,
ContentModule,
FSModule,
GeneralModule, GeneralModule,
ContentModule,
BackupsModule,
NetworkModule, NetworkModule,
StartupModule, StartupModule,
WSModule, WSModule,
FSModule,
} from "./modules/index.ts"; } from "./modules/index.ts";
export function handleError(err: any) { export function handleError(err: any) {
const { addNotification } = injectNotificationManager();
if (err instanceof ModrinthServerError && err.v1Error) { if (err instanceof ModrinthServerError && err.v1Error) {
addNotification({ addNotification({
title: err.v1Error?.context ?? `An error occurred`, title: err.v1Error?.context ?? `An error occurred`,
@@ -1,5 +1,3 @@
import { injectNotificationManager } from "@modrinth/ui";
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>; type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>;
type ErrorFunction = (err: any) => void | Promise<void>; type ErrorFunction = (err: any) => void | Promise<void>;
type VoidFunction = () => void | Promise<void>; type VoidFunction = () => void | Promise<void>;
@@ -11,8 +9,8 @@ type useClientTry = <TArgs extends any[], TResult>(
) => (...args: TArgs) => Promise<TResult | undefined>; ) => (...args: TArgs) => Promise<TResult | undefined>;
const defaultOnError: ErrorFunction = (error) => { const defaultOnError: ErrorFunction = (error) => {
const { addNotification } = injectNotificationManager();
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: error?.data?.description || error.message || error || "Unknown error", text: error?.data?.description || error.message || error || "Unknown error",
type: "error", type: "error",
+5 -4
View File
@@ -135,8 +135,7 @@ export const userFollowProject = async (project) => {
} }
}; };
export const resendVerifyEmail = async () => { export const resendVerifyEmail = async () => {
// const { injectNotificationManager } = await import("@modrinth/ui"); const app = useNuxtApp();
// const { addNotification } = injectNotificationManager();
startLoading(); startLoading();
try { try {
@@ -145,13 +144,15 @@ export const resendVerifyEmail = async () => {
}); });
const auth = await useAuth(); const auth = await useAuth();
addNotification({ app.$notify({
group: "main",
title: "Email sent", title: "Email sent",
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`, text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
type: "success", type: "success",
}); });
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data.description, text: err.data.description,
type: "error", type: "error",
@@ -1,14 +1,19 @@
import { injectNotificationManager } from "@modrinth/ui"; import { useNuxtApp } from "#imports";
import type { Organization, Project, Report, User, Version } from "@modrinth/utils";
// TODO: There needs to be a standardized way to get these types, eg; @modrinth/types generated from api schema. Later problem.
type Project = { id: string };
type Version = { id: string; project_id: string };
type Report = { id: string; item_type: "project" | "user" | "version"; item_id: string };
type Thread = { id: string }; type Thread = { id: string };
type User = { id: string };
type Organization = { id: string };
export type PlatformNotificationAction = { export type NotificationAction = {
title: string; title: string;
action_route: [string, string]; action_route: [string, string];
}; };
export type PlatformNotificationBody = { export type NotificationBody = {
project_id?: string; project_id?: string;
version_id?: string; version_id?: string;
report_id?: string; report_id?: string;
@@ -17,7 +22,7 @@ export type PlatformNotificationBody = {
organization_id?: string; organization_id?: string;
}; };
export type PlatformNotification = { export type Notification = {
id: string; id: string;
user_id: string; user_id: string;
type: "project_update" | "team_invite" | "status_change" | "moderator_message"; type: "project_update" | "team_invite" | "status_change" | "moderator_message";
@@ -26,10 +31,10 @@ export type PlatformNotification = {
link: string; link: string;
read: boolean; read: boolean;
created: string; created: string;
actions: PlatformNotificationAction[]; actions: NotificationAction[];
body?: PlatformNotificationBody; body?: NotificationBody;
extra_data?: Record<string, unknown>; extra_data?: Record<string, unknown>;
grouped_notifs?: PlatformNotification[]; grouped_notifs?: Notification[];
}; };
async function getBulk<T extends { id: string }>( async function getBulk<T extends { id: string }>(
@@ -50,8 +55,8 @@ async function getBulk<T extends { id: string }>(
} }
export async function fetchExtraNotificationData( export async function fetchExtraNotificationData(
notifications: PlatformNotification[], notifications: Notification[],
): Promise<PlatformNotification[]> { ): Promise<Notification[]> {
const bulk = { const bulk = {
projects: [] as string[], projects: [] as string[],
reports: [] as string[], reports: [] as string[],
@@ -128,8 +133,8 @@ export async function fetchExtraNotificationData(
return notifications; return notifications;
} }
export function groupNotifications(notifications: PlatformNotification[]): PlatformNotification[] { export function groupNotifications(notifications: Notification[]): Notification[] {
const grouped: PlatformNotification[] = []; const grouped: Notification[] = [];
for (let i = 0; i < notifications.length; i++) { for (let i = 0; i < notifications.length; i++) {
const current = notifications[i]; const current = notifications[i];
const next = notifications[i + 1]; const next = notifications[i + 1];
@@ -149,18 +154,18 @@ export function groupNotifications(notifications: PlatformNotification[]): Platf
return grouped; return grouped;
} }
function isSimilar(a: PlatformNotification, b: PlatformNotification | undefined): boolean { function isSimilar(a: Notification, b: Notification | undefined): boolean {
return !!a?.body?.project_id && a.body!.project_id === b?.body?.project_id; return !!a?.body?.project_id && a.body!.project_id === b?.body?.project_id;
} }
export async function markAsRead( export async function markAsRead(
ids: string[], ids: string[],
): Promise<(notifications: PlatformNotification[]) => PlatformNotification[]> { ): Promise<(notifications: Notification[]) => Notification[]> {
try { try {
await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, { await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, {
method: "PATCH", method: "PATCH",
}); });
return (notifications: PlatformNotification[]) => { return (notifications: Notification[]) => {
const newNotifs = notifications ?? []; const newNotifs = notifications ?? [];
newNotifs.forEach((n) => { newNotifs.forEach((n) => {
if (ids.includes(n.id)) n.read = true; if (ids.includes(n.id)) n.read = true;
@@ -168,8 +173,9 @@ export async function markAsRead(
return newNotifs; return newNotifs;
}; };
} catch (err: any) { } catch (err: any) {
const { addNotification } = injectNotificationManager(); const app: any = useNuxtApp();
addNotification({ app.$notify({
group: "main",
title: "Error marking notification as read", title: "Error marking notification as read",
text: err?.data?.description ?? err, text: err?.data?.description ?? err,
type: "error", type: "error",
+44 -42
View File
@@ -666,65 +666,65 @@
</template> </template>
<script setup> <script setup>
import { import {
ArrowBigUpDashIcon,
BellIcon,
BlueskyIcon,
BookmarkIcon,
BoxIcon,
BracesIcon,
ChartIcon,
CollectionIcon,
CompassIcon,
CurrencyIcon,
DiscordIcon,
DownloadIcon,
DropdownIcon,
GithubIcon,
GlassesIcon,
HamburgerIcon,
HomeIcon,
IssuesIcon,
LibraryIcon,
LogInIcon,
LogOutIcon,
MastodonIcon,
ModrinthIcon, ModrinthIcon,
ArrowBigUpDashIcon,
BookmarkIcon,
ServerIcon,
LogInIcon,
DownloadIcon,
LibraryIcon,
XIcon,
IssuesIcon,
ReportIcon,
CompassIcon,
HamburgerIcon,
SearchIcon,
BellIcon,
SettingsIcon,
HomeIcon,
MoonIcon, MoonIcon,
OrganizationIcon, SunIcon,
PackageOpenIcon,
PaintbrushIcon,
PlugIcon, PlugIcon,
PlusIcon, PlusIcon,
ReportIcon, DropdownIcon,
ScaleIcon, LogOutIcon,
SearchIcon, ChartIcon,
ServerIcon, BoxIcon,
SettingsIcon, CollectionIcon,
SunIcon, OrganizationIcon,
TwitterIcon,
UserIcon, UserIcon,
XIcon, CurrencyIcon,
BracesIcon,
GlassesIcon,
PaintbrushIcon,
PackageOpenIcon,
DiscordIcon,
BlueskyIcon,
TwitterIcon,
MastodonIcon,
GithubIcon,
ScaleIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
Avatar,
Button, Button,
ButtonStyled, ButtonStyled,
commonMessages,
injectNotificationManager,
OverflowMenu, OverflowMenu,
PagewideBanner, PagewideBanner,
Avatar,
commonMessages,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { isAdmin, isStaff } from "@modrinth/utils"; import { isAdmin, isStaff } from "@modrinth/utils";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue"; import { errors as generatedStateErrors } from "~/generated/state.json";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue"; import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue";
import TeleportOverflowMenu from "~/components/ui/servers/TeleportOverflowMenu.vue"; import TeleportOverflowMenu from "~/components/ui/servers/TeleportOverflowMenu.vue";
import { errors as generatedStateErrors } from "~/generated/state.json";
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
const app = useNuxtApp();
const auth = await useAuth(); const auth = await useAuth();
const user = await useUser(); const user = await useUser();
@@ -1079,13 +1079,15 @@ function developerModeIncrement() {
developerModeCounter.value = 0; developerModeCounter.value = 0;
saveFeatureFlags(); saveFeatureFlags();
if (flags.value.developerMode) { if (flags.value.developerMode) {
addNotification({ app.$notify({
group: "main",
title: "Developer mode activated", title: "Developer mode activated",
text: "Developer mode has been enabled", text: "Developer mode has been enabled",
type: "success", type: "success",
}); });
} else { } else {
addNotification({ app.$notify({
group: "main",
title: "Developer mode deactivated", title: "Developer mode deactivated",
text: "Developer mode has been disabled", text: "Developer mode has been disabled",
type: "success", type: "success",
@@ -554,6 +554,78 @@
"profile.user-id": { "profile.user-id": {
"message": "User ID: {id}" "message": "User ID: {id}"
}, },
"project-member-header.accept": {
"message": "Accept"
},
"project-member-header.action": {
"message": "Action"
},
"project-member-header.decline": {
"message": "Decline"
},
"project-member-header.error": {
"message": "Error"
},
"project-member-header.error-decline": {
"message": "Failed to decline team invitation"
},
"project-member-header.error-join": {
"message": "Failed to accept team invitation"
},
"project-member-header.invitation-no-role": {
"message": "You've been invited to join this project. Please accept or decline the invitation."
},
"project-member-header.invitation-title": {
"message": "Invitation to join project"
},
"project-member-header.invitation-with-role": {
"message": "You've been invited be a member of this project with the role of '{role}'."
},
"project-member-header.key-title": {
"message": "Status Key"
},
"project-member-header.publishing-checklist": {
"message": "Publishing checklist"
},
"project-member-header.required": {
"message": "Required"
},
"project-member-header.resubmit-for-review": {
"message": "Resubmit for review"
},
"project-member-header.resubmit-for-review-desc": {
"message": "Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message."
},
"project-member-header.show-key": {
"message": "Toggle key"
},
"project-member-header.submit-checklist-tooltip": {
"message": "You must complete the required steps in the publishing checklist!"
},
"project-member-header.submit-for-review": {
"message": "Submit for review"
},
"project-member-header.submit-for-review-desc": {
"message": "Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published."
},
"project-member-header.success": {
"message": "Success"
},
"project-member-header.success-decline": {
"message": "You have declined the team invitation"
},
"project-member-header.success-join": {
"message": "You have joined the project team"
},
"project-member-header.suggestion": {
"message": "Suggestion"
},
"project-member-header.visit-moderation-page": {
"message": "Visit moderation page"
},
"project-member-header.warning": {
"message": "Warning"
},
"project-type.collection.plural": { "project-type.collection.plural": {
"message": "Collections" "message": "Collections"
}, },
+19 -20
View File
@@ -890,7 +890,6 @@
</template> </template>
<script setup> <script setup>
import { navigateTo } from "#app";
import { import {
BookmarkIcon, BookmarkIcon,
BookTextIcon, BookTextIcon,
@@ -907,7 +906,6 @@ import {
HeartIcon, HeartIcon,
InfoIcon, InfoIcon,
LinkIcon as LinksIcon, LinkIcon as LinksIcon,
ModrinthIcon,
MoreVerticalIcon, MoreVerticalIcon,
PlusIcon, PlusIcon,
ReportIcon, ReportIcon,
@@ -919,13 +917,13 @@ import {
UsersIcon, UsersIcon,
VersionIcon, VersionIcon,
WrenchIcon, WrenchIcon,
ModrinthIcon,
XIcon, XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
Avatar, Avatar,
ButtonStyled, ButtonStyled,
Checkbox, Checkbox,
injectNotificationManager,
NewModal, NewModal,
OverflowMenu, OverflowMenu,
PopoutMenu, PopoutMenu,
@@ -937,37 +935,36 @@ import {
ProjectSidebarLinks, ProjectSidebarLinks,
ProjectStatusBadge, ProjectStatusBadge,
ScrollablePanel, ScrollablePanel,
ServersPromo,
TagItem, TagItem,
ServersPromo,
useRelativeTime, useRelativeTime,
} from "@modrinth/ui"; } from "@modrinth/ui";
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue"; import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
import { formatCategory, formatProjectType, renderString } from "@modrinth/utils"; import { formatCategory, formatProjectType, renderString } from "@modrinth/utils";
import { useLocalStorage } from "@vueuse/core";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Tooltip } from "floating-vue"; import { Tooltip } from "floating-vue";
import { useLocalStorage } from "@vueuse/core";
import { navigateTo } from "#app";
import Accordion from "~/components/ui/Accordion.vue"; import Accordion from "~/components/ui/Accordion.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue"; import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue"; import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import MessageBanner from "~/components/ui/MessageBanner.vue"; import MessageBanner from "~/components/ui/MessageBanner.vue";
import ModerationChecklist from "~/components/ui/moderation/checklist/ModerationChecklist.vue";
import NavStack from "~/components/ui/NavStack.vue"; import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue"; import NavStackItem from "~/components/ui/NavStackItem.vue";
import NavTabs from "~/components/ui/NavTabs.vue"; import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue"; import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
import { userCollectProject } from "~/composables/user.js"; import { userCollectProject } from "~/composables/user.js";
import { useModerationStore } from "~/store/moderation.ts";
import { reportProject } from "~/utils/report-helpers.ts"; import { reportProject } from "~/utils/report-helpers.ts";
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
import ModerationChecklist from "~/components/ui/moderation/checklist/ModerationChecklist.vue";
import { useModerationStore } from "~/store/moderation.ts";
const data = useNuxtApp(); const data = useNuxtApp();
const route = useNativeRoute(); const route = useNativeRoute();
const config = useRuntimeConfig(); const config = useRuntimeConfig();
const moderationStore = useModerationStore(); const moderationStore = useModerationStore();
const notifications = injectNotificationManager();
const { addNotification } = notifications;
const auth = await useAuth(); const auth = await useAuth();
const user = await useUser(); const user = await useUser();
@@ -977,6 +974,7 @@ const flags = useFeatureFlags();
const cosmetics = useCosmetics(); const cosmetics = useCosmetics();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
const { setVisible } = useNotificationRightwards();
const settingsModal = ref(); const settingsModal = ref();
const downloadModal = ref(); const downloadModal = ref();
@@ -1427,7 +1425,8 @@ async function setProcessing() {
project.value.status = "processing"; project.value.status = "processing";
} catch (err) { } catch (err) {
addNotification({ data.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -1460,7 +1459,8 @@ async function patchProject(resData, quiet = false) {
result = true; result = true;
if (!quiet) { if (!quiet) {
addNotification({ data.$notify({
group: "main",
title: "Project updated", title: "Project updated",
text: "Your project has been updated.", text: "Your project has been updated.",
type: "success", type: "success",
@@ -1468,7 +1468,8 @@ async function patchProject(resData, quiet = false) {
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
} catch (err) { } catch (err) {
addNotification({ data.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -1497,13 +1498,15 @@ async function patchIcon(icon) {
); );
await resetProject(); await resetProject();
result = true; result = true;
addNotification({ data.$notify({
group: "main",
title: "Project icon updated", title: "Project icon updated",
text: "Your project's icon has been updated.", text: "Your project's icon has been updated.",
type: "success", type: "success",
}); });
} catch (err) { } catch (err) {
addNotification({ data.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -1552,15 +1555,11 @@ const collapsedModerationChecklist = useLocalStorage("collapsed-moderation-check
watch( watch(
showModerationChecklist, showModerationChecklist,
(newValue) => { (newValue) => {
notifications.setNotificationLocation(newValue ? "left" : "right"); setVisible(newValue);
}, },
{ immediate: true }, { immediate: true },
); );
onUnmounted(() => {
notifications.setNotificationLocation("right");
});
if (import.meta.client && history && history.state && history.state.showChecklist) { if (import.meta.client && history && history.state && history.state.showChecklist) {
showModerationChecklist.value = true; showModerationChecklist.value = true;
} }
+20 -23
View File
@@ -278,26 +278,26 @@
<script setup> <script setup>
import { import {
CalendarIcon,
ContractIcon,
EditIcon,
ExpandIcon,
ExternalIcon,
ImageIcon,
InfoIcon,
LeftArrowIcon,
PlusIcon, PlusIcon,
RightArrowIcon, CalendarIcon,
EditIcon,
TrashIcon,
SaveIcon, SaveIcon,
StarIcon, StarIcon,
TransferIcon,
TrashIcon,
UploadIcon,
XIcon, XIcon,
RightArrowIcon,
LeftArrowIcon,
ExternalIcon,
ExpandIcon,
ContractIcon,
UploadIcon,
InfoIcon,
ImageIcon,
TransferIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { ConfirmModal, injectNotificationManager } from "@modrinth/ui"; import { ConfirmModal } from "@modrinth/ui";
import DropArea from "~/components/ui/DropArea.vue";
import FileInput from "~/components/ui/FileInput.vue"; import FileInput from "~/components/ui/FileInput.vue";
import DropArea from "~/components/ui/DropArea.vue";
import Modal from "~/components/ui/Modal.vue"; import Modal from "~/components/ui/Modal.vue";
import { isPermission } from "~/utils/permissions.ts"; import { isPermission } from "~/utils/permissions.ts";
@@ -425,8 +425,6 @@ export default defineNuxtComponent({
this.shouldPreventActions = true; this.shouldPreventActions = true;
startLoading(); startLoading();
const { addNotification } = injectNotificationManager();
try { try {
let url = `project/${this.project.id}/gallery?ext=${ let url = `project/${this.project.id}/gallery?ext=${
this.editFile this.editFile
@@ -452,7 +450,8 @@ export default defineNuxtComponent({
this.$refs.modal_edit_item.hide(); this.$refs.modal_edit_item.hide();
} catch (err) { } catch (err) {
addNotification({ this.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -466,8 +465,6 @@ export default defineNuxtComponent({
this.shouldPreventActions = true; this.shouldPreventActions = true;
startLoading(); startLoading();
const { addNotification } = injectNotificationManager();
try { try {
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent( let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
this.project.gallery[this.editIndex].url, this.project.gallery[this.editIndex].url,
@@ -490,7 +487,8 @@ export default defineNuxtComponent({
await this.resetProject(); await this.resetProject();
this.$refs.modal_edit_item.hide(); this.$refs.modal_edit_item.hide();
} catch (err) { } catch (err) {
addNotification({ this.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -503,8 +501,6 @@ export default defineNuxtComponent({
async deleteGalleryImage() { async deleteGalleryImage() {
startLoading(); startLoading();
const { addNotification } = injectNotificationManager();
try { try {
await useBaseFetch( await useBaseFetch(
`project/${this.project.id}/gallery?url=${encodeURIComponent( `project/${this.project.id}/gallery?url=${encodeURIComponent(
@@ -517,7 +513,8 @@ export default defineNuxtComponent({
await this.resetProject(); await this.resetProject();
} catch (err) { } catch (err) {
addNotification({ this.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -99,8 +99,8 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { CheckIcon, IssuesIcon, XIcon } from "@modrinth/assets"; import { XIcon, CheckIcon, IssuesIcon } from "@modrinth/assets";
import { Badge, injectNotificationManager } from "@modrinth/ui"; import { Badge } from "@modrinth/ui";
import ConversationThread from "~/components/ui/thread/ConversationThread.vue"; import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
import { import {
getProjectLink, getProjectLink,
@@ -111,7 +111,6 @@ import {
isUnderReview, isUnderReview,
} from "~/helpers/projects.js"; } from "~/helpers/projects.js";
const { addNotification } = injectNotificationManager();
const props = defineProps({ const props = defineProps({
project: { project: {
type: Object, type: Object,
@@ -132,6 +131,7 @@ const props = defineProps({
}, },
}); });
const app = useNuxtApp();
const auth = await useAuth(); const auth = await useAuth();
const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () => const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () =>
@@ -153,7 +153,8 @@ async function setStatus(status) {
await props.resetProject(); await props.resetProject();
thread.value = await useBaseFetch(`thread/${thread.value.id}`); thread.value = await useBaseFetch(`thread/${thread.value.id}`);
} catch (err) { } catch (err) {
addNotification({ app.$notify({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -22,6 +22,10 @@
" "
:on-image-upload="onUploadHandler" :on-image-upload="onUploadHandler"
/> />
<div v-if="descriptionWarning" class="flex items-center gap-1.5 text-orange">
<TriangleAlertIcon class="my-auto" />
{{ descriptionWarning }}
</div>
<div class="input-group markdown-disclaimer"> <div class="input-group markdown-disclaimer">
<button <button
:disabled="!hasChanges" :disabled="!hasChanges"
@@ -38,7 +42,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { SaveIcon } from "@modrinth/assets"; import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
import { countText, MIN_DESCRIPTION_CHARS } from "@modrinth/moderation";
import { MarkdownEditor } from "@modrinth/ui"; import { MarkdownEditor } from "@modrinth/ui";
import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils"; import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
@@ -53,6 +58,17 @@ const props = defineProps<{
const description = ref(props.project.body); const description = ref(props.project.body);
const descriptionWarning = computed(() => {
const text = description.value?.trim() || "";
const charCount = countText(text);
if (charCount < MIN_DESCRIPTION_CHARS) {
return `It's recommended to have a description with at least ${MIN_DESCRIPTION_CHARS} readable characters. (${charCount}/${MIN_DESCRIPTION_CHARS})`;
}
return null;
});
const patchRequestPayload = computed(() => { const patchRequestPayload = computed(() => {
const payload: { const payload: {
body?: string; body?: string;
@@ -82,6 +82,10 @@
<label for="project-summary"> <label for="project-summary">
<span class="label__title">Summary</span> <span class="label__title">Summary</span>
</label> </label>
<div v-if="summaryWarning" class="my-2 flex items-center gap-1.5 text-orange">
<TriangleAlertIcon class="my-auto" />
{{ summaryWarning }}
</div>
<div class="textarea-wrapper summary-input"> <div class="textarea-wrapper summary-input">
<textarea <textarea
id="project-summary" id="project-summary"
@@ -239,14 +243,21 @@
</template> </template>
<script setup> <script setup>
import { CheckIcon, IssuesIcon, SaveIcon, TrashIcon, UploadIcon, XIcon } from "@modrinth/assets";
import { Avatar, ConfirmModal, injectNotificationManager } from "@modrinth/ui";
import { formatProjectStatus, formatProjectType } from "@modrinth/utils"; import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
import {
UploadIcon,
SaveIcon,
TrashIcon,
XIcon,
IssuesIcon,
CheckIcon,
TriangleAlertIcon,
} from "@modrinth/assets";
import { Multiselect } from "vue-multiselect"; import { Multiselect } from "vue-multiselect";
import { ConfirmModal, Avatar } from "@modrinth/ui";
import { MIN_SUMMARY_CHARS } from "@modrinth/moderation";
import FileInput from "~/components/ui/FileInput.vue"; import FileInput from "~/components/ui/FileInput.vue";
const { addNotification } = injectNotificationManager();
const props = defineProps({ const props = defineProps({
project: { project: {
type: Object, type: Object,
@@ -302,6 +313,17 @@ const hasDeletePermission = computed(() => {
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT; return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT;
}); });
const summaryWarning = computed(() => {
const text = summary.value?.trim() || "";
const charCount = text.length;
if (charCount < MIN_SUMMARY_CHARS) {
return `It's recommended to have a summary with at least ${MIN_SUMMARY_CHARS} characters. (${charCount}/${MIN_SUMMARY_CHARS})`;
}
return null;
});
const sideTypes = ["required", "optional", "unsupported"]; const sideTypes = ["required", "optional", "unsupported"];
const patchData = computed(() => { const patchData = computed(() => {
@@ -376,6 +398,7 @@ const deleteProject = async () => {
await initUserProjects(); await initUserProjects();
await router.push("/dashboard/projects"); await router.push("/dashboard/projects");
addNotification({ addNotification({
group: "main",
title: "Project deleted", title: "Project deleted",
text: "Your project has been deleted.", text: "Your project has been deleted.",
type: "success", type: "success",
@@ -394,6 +417,7 @@ const deleteIcon = async () => {
}); });
await props.resetProject(); await props.resetProject();
addNotification({ addNotification({
group: "main",
title: "Project icon removed", title: "Project icon removed",
text: "Your project's icon has been removed.", text: "Your project's icon has been removed.",
type: "success", type: "success",
@@ -7,11 +7,26 @@
id="project-issue-tracker" id="project-issue-tracker"
title="A place for users to report bugs, issues, and concerns about your project." title="A place for users to report bugs, issues, and concerns about your project."
> >
<span class="label__title">Issue tracker</span> <span class="label__title">Issue tracker </span>
<span class="label__description"> <span class="label__description">
A place for users to report bugs, issues, and concerns about your project. A place for users to report bugs, issues, and concerns about your project.
</span> </span>
</label> </label>
<TriangleAlertIcon
v-if="isIssuesLinkShortener"
v-tooltip="`Use of link shorteners is prohibited.`"
class="size-6 animate-pulse text-orange"
/>
<TriangleAlertIcon
v-else-if="isIssuesDiscordUrl"
v-tooltip="`Discord invites are not appropriate for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<TriangleAlertIcon
v-else-if="!isIssuesUrlCommon"
v-tooltip="`Link includes a domain which isn't common for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<input <input
id="project-issue-tracker" id="project-issue-tracker"
v-model="issuesUrl" v-model="issuesUrl"
@@ -26,11 +41,26 @@
id="project-source-code" id="project-source-code"
title="A page/repository containing the source code for your project" title="A page/repository containing the source code for your project"
> >
<span class="label__title">Source code</span> <span class="label__title">Source code </span>
<span class="label__description"> <span class="label__description">
A page/repository containing the source code for your project A page/repository containing the source code for your project
</span> </span>
</label> </label>
<TriangleAlertIcon
v-if="isSourceLinkShortener"
v-tooltip="`Use of link shorteners is prohibited.`"
class="size-6 animate-pulse text-orange"
/>
<TriangleAlertIcon
v-else-if="isSourceDiscordUrl"
v-tooltip="`Discord invites are not appropriate for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<TriangleAlertIcon
v-else-if="!isSourceUrlCommon"
v-tooltip="`Link includes a domain which isn't common for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<input <input
id="project-source-code" id="project-source-code"
v-model="sourceUrl" v-model="sourceUrl"
@@ -50,6 +80,16 @@
A page containing information, documentation, and help for the project. A page containing information, documentation, and help for the project.
</span> </span>
</label> </label>
<TriangleAlertIcon
v-if="isWikiLinkShortener"
v-tooltip="`Use of link shorteners is prohibited.`"
class="size-6 animate-pulse text-orange"
/>
<TriangleAlertIcon
v-else-if="isWikiDiscordUrl"
v-tooltip="`Discord invites are not appropriate for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<input <input
id="project-wiki-page" id="project-wiki-page"
v-model="wikiUrl" v-model="wikiUrl"
@@ -61,9 +101,19 @@
</div> </div>
<div class="adjacent-input"> <div class="adjacent-input">
<label id="project-discord-invite" title="An invitation link to your Discord server."> <label id="project-discord-invite" title="An invitation link to your Discord server.">
<span class="label__title">Discord invite</span> <span class="label__title">Discord invite </span>
<span class="label__description"> An invitation link to your Discord server. </span> <span class="label__description"> An invitation link to your Discord server. </span>
</label> </label>
<TriangleAlertIcon
v-if="isDiscordLinkShortener"
v-tooltip="`Use of link shorteners is prohibited.`"
class="size-6 animate-pulse text-orange"
/>
<TriangleAlertIcon
v-else-if="!isDiscordUrlCommon"
v-tooltip="`You're using a link which isn't common for this link type.`"
class="size-6 animate-pulse text-orange"
/>
<input <input
id="project-discord-invite" id="project-discord-invite"
v-model="discordUrl" v-model="discordUrl"
@@ -123,7 +173,13 @@
<script setup> <script setup>
import { DropdownSelect } from "@modrinth/ui"; import { DropdownSelect } from "@modrinth/ui";
import { SaveIcon } from "@modrinth/assets"; import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
import {
isCommonUrl,
isDiscordUrl,
isLinkShortener,
commonLinkDomains,
} from "@modrinth/moderation";
const tags = useTags(); const tags = useTags();
@@ -153,6 +209,46 @@ const sourceUrl = ref(props.project.source_url);
const wikiUrl = ref(props.project.wiki_url); const wikiUrl = ref(props.project.wiki_url);
const discordUrl = ref(props.project.discord_url); const discordUrl = ref(props.project.discord_url);
const isIssuesUrlCommon = computed(() => {
if (!issuesUrl.value || issuesUrl.value.trim().length === 0) return true;
return isCommonUrl(issuesUrl.value, commonLinkDomains.issues);
});
const isSourceUrlCommon = computed(() => {
if (!sourceUrl.value || sourceUrl.value.trim().length === 0) return true;
return isCommonUrl(sourceUrl.value, commonLinkDomains.source);
});
const isDiscordUrlCommon = computed(() => {
if (!discordUrl.value || discordUrl.value.trim().length === 0) return true;
return isCommonUrl(discordUrl.value, commonLinkDomains.discord);
});
const isIssuesDiscordUrl = computed(() => {
return isDiscordUrl(issuesUrl.value);
});
const isSourceDiscordUrl = computed(() => {
return isDiscordUrl(sourceUrl.value);
});
const isWikiDiscordUrl = computed(() => {
return isDiscordUrl(wikiUrl.value);
});
const isIssuesLinkShortener = computed(() => {
return isLinkShortener(issuesUrl.value);
});
const isSourceLinkShortener = computed(() => {
return isLinkShortener(sourceUrl.value);
});
const isWikiLinkShortener = computed(() => {
return isLinkShortener(wikiUrl.value);
});
const isDiscordLinkShortener = computed(() => {
return isLinkShortener(discordUrl.value);
});
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls)); const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls));
rawDonationLinks.push({ rawDonationLinks.push({
id: null, id: null,
@@ -518,30 +518,21 @@
</template> </template>
<script setup> <script setup>
import {
CheckIcon,
CrownIcon,
DropdownIcon,
OrganizationIcon,
SaveIcon,
TransferIcon,
UserPlusIcon,
UsersIcon,
UserXIcon,
} from "@modrinth/assets";
import {
Avatar,
Badge,
Card,
Checkbox,
ConfirmModal,
injectNotificationManager,
} from "@modrinth/ui";
import { Multiselect } from "vue-multiselect"; import { Multiselect } from "vue-multiselect";
import {
TransferIcon,
CheckIcon,
UsersIcon,
DropdownIcon,
SaveIcon,
UserPlusIcon,
UserXIcon,
OrganizationIcon,
CrownIcon,
} from "@modrinth/assets";
import { Avatar, Badge, Card, Checkbox, ConfirmModal } from "@modrinth/ui";
import { removeSelfFromTeam } from "~/helpers/teams.js"; import { removeSelfFromTeam } from "~/helpers/teams.js";
const { addNotification } = injectNotificationManager();
const props = defineProps({ const props = defineProps({
project: { project: {
type: Object, type: Object,
@@ -663,6 +654,7 @@ const onAddToOrg = useClientTry(async () => {
await updateMembers(); await updateMembers();
addNotification({ addNotification({
group: "main",
title: "Project transferred", title: "Project transferred",
text: "Your project has been transferred to the organization.", text: "Your project has been transferred to the organization.",
type: "success", type: "success",
@@ -683,6 +675,7 @@ const onRemoveFromOrg = useClientTry(async () => {
await updateMembers(); await updateMembers();
addNotification({ addNotification({
group: "main",
title: "Project removed", title: "Project removed",
text: "Your project has been removed from the organization.", text: "Your project has been removed from the organization.",
type: "success", type: "success",
@@ -710,6 +703,7 @@ const inviteTeamMember = async () => {
await updateMembers(); await updateMembers();
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error", text: err?.data?.description || err?.message || err || "Unknown error",
type: "error", type: "error",
@@ -732,6 +726,7 @@ const removeTeamMember = async (index) => {
await updateMembers(); await updateMembers();
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error", text: err?.data?.description || err?.message || err || "Unknown error",
type: "error", type: "error",
@@ -765,12 +760,14 @@ const updateTeamMember = async (index) => {
); );
await updateMembers(); await updateMembers();
addNotification({ addNotification({
group: "main",
title: "Member(s) updated", title: "Member(s) updated",
text: "Your project's member(s) has been updated.", text: "Your project's member(s) has been updated.",
type: "success", type: "success",
}); });
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error", text: err?.data?.description || err?.message || err || "Unknown error",
type: "error", type: "error",
@@ -793,6 +790,7 @@ const transferOwnership = async (index) => {
await updateMembers(); await updateMembers();
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error", text: err?.data?.description || err?.message || err || "Unknown error",
type: "error", type: "error",
@@ -839,6 +837,7 @@ async function updateOrgMember(index) {
await updateMembers(); await updateMembers();
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err?.data?.description || err?.message || err || "Unknown error", text: err?.data?.description || err?.message || err || "Unknown error",
type: "error", type: "error",
@@ -6,11 +6,31 @@
<span class="label__title size-card-header">Tags</span> <span class="label__title size-card-header">Tags</span>
</h3> </h3>
</div> </div>
<div
v-if="tooManyTagsWarning && !allTagsSelectedWarning"
class="my-2 flex items-center gap-1.5 text-orange"
>
<TriangleAlertIcon class="my-auto" />
{{ tooManyTagsWarning }}
</div>
<div v-if="multipleResolutionTagsWarning" class="my-2 flex items-center gap-1.5 text-orange">
<TriangleAlertIcon class="my-auto" />
{{ multipleResolutionTagsWarning }}
</div>
<div v-if="allTagsSelectedWarning" class="my-2 flex items-center gap-1.5 text-red">
<TriangleAlertIcon class="my-auto" />
<span>{{ allTagsSelectedWarning }}</span>
</div>
<p> <p>
Accurate tagging is important to help people find your Accurate tagging is important to help people find your
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags {{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
that apply. that apply.
</p> </p>
<p v-if="project.versions.length === 0" class="known-errors"> <p v-if="project.versions.length === 0" class="known-errors">
Please upload a version first in order to select tags! Please upload a version first in order to select tags!
</p> </p>
@@ -112,146 +132,188 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { SaveIcon, StarIcon } from "@modrinth/assets"; import { computed, ref } from "vue";
import { injectNotificationManager } from "@modrinth/ui"; import { StarIcon, SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils"; import {
formatCategory,
formatCategoryHeader,
formatProjectType,
sortedCategories,
type Project,
} from "@modrinth/utils";
import Checkbox from "~/components/ui/Checkbox.vue"; import Checkbox from "~/components/ui/Checkbox.vue";
export default defineNuxtComponent({ interface Category {
components: { name: string;
Checkbox, header: string;
SaveIcon, icon?: string;
StarIcon, project_type: string;
}, }
props: {
project: {
type: Object,
default() {
return {};
},
},
allMembers: {
type: Array,
default() {
return [];
},
},
currentMember: {
type: Object,
default() {
return null;
},
},
patchProject: {
type: Function,
default() {
return () => {
const { addNotification } = injectNotificationManager();
addNotification({
title: "An error occurred",
text: "Patch project function not found",
type: "error",
});
};
},
},
},
data() {
return {
selectedTags: this.$sortedCategories().filter(
(x) =>
x.project_type === this.project.actualProjectType &&
(this.project.categories.includes(x.name) ||
this.project.additional_categories.includes(x.name)),
),
featuredTags: this.$sortedCategories().filter(
(x) =>
x.project_type === this.project.actualProjectType &&
this.project.categories.includes(x.name),
),
};
},
computed: {
categoryLists() {
const lists = {};
this.$sortedCategories().forEach((x) => {
if (x.project_type === this.project.actualProjectType) {
const header = x.header;
if (!lists[header]) {
lists[header] = [];
}
lists[header].push(x);
}
});
return lists;
},
patchData() {
const data = {};
// Promote selected categories to featured if there are less than 3 featured
const newFeaturedTags = this.featuredTags.slice();
if (newFeaturedTags.length < 1 && this.selectedTags.length > newFeaturedTags.length) {
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x));
nonFeaturedCategories interface Props {
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length)) project: Project & {
.forEach((x) => newFeaturedTags.push(x)); actualProjectType: string;
} };
// Convert selected and featured categories to backend-usable arrays allMembers?: any[];
const categories = newFeaturedTags.map((x) => x.name); currentMember?: any;
const additionalCategories = this.selectedTags patchProject?: (data: any) => void;
.filter((x) => !newFeaturedTags.includes(x)) }
.map((x) => x.name);
if ( const tags = useTags();
categories.length !== this.project.categories.length ||
categories.some((value) => !this.project.categories.includes(value))
) {
data.categories = categories;
}
if ( const props = withDefaults(defineProps<Props>(), {
additionalCategories.length !== this.project.additional_categories.length || allMembers: () => [],
additionalCategories.some((value) => !this.project.additional_categories.includes(value)) currentMember: null,
) { patchProject: () => {
data.additional_categories = additionalCategories; addNotification({
} title: "An error occurred",
text: "Patch project function not found",
return data; type: "error",
}, });
hasChanges() {
return Object.keys(this.patchData).length > 0;
},
},
methods: {
formatProjectType,
formatCategoryHeader,
formatCategory,
toggleCategory(category) {
if (this.selectedTags.includes(category)) {
this.selectedTags = this.selectedTags.filter((x) => x !== category);
if (this.featuredTags.includes(category)) {
this.featuredTags = this.featuredTags.filter((x) => x !== category);
}
} else {
this.selectedTags.push(category);
}
},
toggleFeaturedCategory(category) {
if (this.featuredTags.includes(category)) {
this.featuredTags = this.featuredTags.filter((x) => x !== category);
} else {
this.featuredTags.push(category);
}
},
saveChanges() {
if (this.hasChanges) {
this.patchProject(this.patchData);
}
},
}, },
}); });
const selectedTags = ref<Category[]>(
sortedCategories(tags.value).filter(
(x: Category) =>
x.project_type === props.project.actualProjectType &&
(props.project.categories.includes(x.name) ||
props.project.additional_categories.includes(x.name)),
),
);
const featuredTags = ref<Category[]>(
sortedCategories(tags.value).filter(
(x: Category) =>
x.project_type === props.project.actualProjectType &&
props.project.categories.includes(x.name),
),
);
const categoryLists = computed(() => {
const lists: Record<string, Category[]> = {};
sortedCategories(tags.value).forEach((x: Category) => {
if (x.project_type === props.project.actualProjectType) {
const header = x.header;
if (!lists[header]) {
lists[header] = [];
}
lists[header].push(x);
}
});
return lists;
});
const tooManyTagsWarning = computed(() => {
const tagCount = selectedTags.value.length;
if (tagCount > 8) {
return `You've selected ${tagCount} tags. Consider reducing to 8 or fewer to keep your project focused and easier to discover.`;
}
return null;
});
const multipleResolutionTagsWarning = computed(() => {
if (props.project.project_type !== "resourcepack") return null;
const resolutionTags = selectedTags.value.filter((tag) =>
["8x-", "16x", "32x", "48x", "64x", "128x", "256x", "512x+"].includes(tag.name),
);
if (resolutionTags.length > 1) {
return `You've selected ${resolutionTags.length} resolution tags (${resolutionTags
.map((t) => t.name)
.join(", ")
.replace("8x-", "8x or lower")
.replace(
"512x+",
"512x or higher",
)}). Resource packs should typically only have one resolution tag.`;
}
return null;
});
const allTagsSelectedWarning = computed(() => {
const categoriesForProjectType = sortedCategories(tags.value).filter(
(x: Category) => x.project_type === props.project.actualProjectType,
);
const totalSelectedTags = selectedTags.value.length;
if (
totalSelectedTags === categoriesForProjectType.length &&
categoriesForProjectType.length > 0
) {
return `You've selected all ${categoriesForProjectType.length} available tags. Please select only the tags that truly apply to your project.`;
}
return null;
});
const patchData = computed(() => {
const data: Record<string, string[]> = {};
// Promote selected categories to featured if there are less than 3 featured
const newFeaturedTags = featuredTags.value.slice();
if (newFeaturedTags.length < 1 && selectedTags.value.length > newFeaturedTags.length) {
const nonFeaturedCategories = selectedTags.value.filter((x) => !newFeaturedTags.includes(x));
nonFeaturedCategories
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
.forEach((x) => newFeaturedTags.push(x));
}
// Convert selected and featured categories to backend-usable arrays
const categories = newFeaturedTags.map((x) => x.name);
const additionalCategories = selectedTags.value
.filter((x) => !newFeaturedTags.includes(x))
.map((x) => x.name);
if (
categories.length !== props.project.categories.length ||
categories.some((value) => !props.project.categories.includes(value))
) {
data.categories = categories;
}
if (
additionalCategories.length !== props.project.additional_categories.length ||
additionalCategories.some((value) => !props.project.additional_categories.includes(value))
) {
data.additional_categories = additionalCategories;
}
return data;
});
const hasChanges = computed(() => {
return Object.keys(patchData.value).length > 0;
});
const toggleCategory = (category: Category) => {
if (selectedTags.value.includes(category)) {
selectedTags.value = selectedTags.value.filter((x) => x !== category);
if (featuredTags.value.includes(category)) {
featuredTags.value = featuredTags.value.filter((x) => x !== category);
}
} else {
selectedTags.value.push(category);
}
};
const toggleFeaturedCategory = (category: Category) => {
if (featuredTags.value.includes(category)) {
featuredTags.value = featuredTags.value.filter((x) => x !== category);
} else {
featuredTags.value.push(category);
}
};
const saveChanges = () => {
if (hasChanges.value) {
props.patchProject(patchData.value);
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.label__title { .label__title {
display: flex; display: flex;
@@ -630,47 +630,46 @@
</div> </div>
</template> </template>
<script> <script>
import {
BoxIcon,
ChevronRightIcon,
DownloadIcon,
EditIcon,
FileIcon,
HashIcon,
PlusIcon,
ReportIcon,
RightArrowIcon,
SaveIcon,
StarIcon,
TransferIcon,
TrashIcon,
UploadIcon,
XIcon,
} from "@modrinth/assets";
import { import {
Avatar, Avatar,
Badge, Badge,
ButtonStyled,
Checkbox,
ConfirmModal,
CopyCode, CopyCode,
injectNotificationManager, Checkbox,
ButtonStyled,
ConfirmModal,
MarkdownEditor, MarkdownEditor,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { formatBytes, formatCategory } from "@modrinth/utils"; import {
FileIcon,
TrashIcon,
EditIcon,
DownloadIcon,
StarIcon,
ReportIcon,
SaveIcon,
XIcon,
HashIcon,
PlusIcon,
TransferIcon,
UploadIcon,
BoxIcon,
RightArrowIcon,
ChevronRightIcon,
} from "@modrinth/assets";
import { Multiselect } from "vue-multiselect"; import { Multiselect } from "vue-multiselect";
import { useImageUpload } from "~/composables/image-upload.ts"; import { formatBytes, formatCategory } from "@modrinth/utils";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js"; import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
import { renderHighlightedString } from "~/helpers/highlight.js";
import { inferVersionInfo } from "~/helpers/infer.js"; import { inferVersionInfo } from "~/helpers/infer.js";
import { createDataPackVersion } from "~/helpers/package.js"; import { createDataPackVersion } from "~/helpers/package.js";
import { renderHighlightedString } from "~/helpers/highlight.js";
import { reportVersion } from "~/utils/report-helpers.ts"; import { reportVersion } from "~/utils/report-helpers.ts";
import { useImageUpload } from "~/composables/image-upload.ts";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import Categories from "~/components/ui/search/Categories.vue";
import FileInput from "~/components/ui/FileInput.vue"; import FileInput from "~/components/ui/FileInput.vue";
import Modal from "~/components/ui/Modal.vue"; import Modal from "~/components/ui/Modal.vue";
import Categories from "~/components/ui/search/Categories.vue";
export default defineNuxtComponent({ export default defineNuxtComponent({
components: { components: {
@@ -998,8 +997,8 @@ export default defineNuxtComponent({
const project = await useBaseFetch(`project/${newDependencyId}`); const project = await useBaseFetch(`project/${newDependencyId}`);
if (this.version.dependencies.some((dep) => project.id === dep.project_id)) { if (this.version.dependencies.some((dep) => project.id === dep.project_id)) {
const { addNotification } = injectNotificationManager(); this.$notify({
addNotification({ group: "main",
title: "Dependency already added", title: "Dependency already added",
text: "You cannot add the same dependency twice.", text: "You cannot add the same dependency twice.",
type: "error", type: "error",
@@ -1023,8 +1022,8 @@ export default defineNuxtComponent({
const project = await useBaseFetch(`project/${version.project_id}`); const project = await useBaseFetch(`project/${version.project_id}`);
if (this.version.dependencies.some((dep) => version.id === dep.version_id)) { if (this.version.dependencies.some((dep) => version.id === dep.version_id)) {
const { addNotification } = injectNotificationManager(); this.$notify({
addNotification({ group: "main",
title: "Dependency already added", title: "Dependency already added",
text: "You cannot add the same dependency twice.", text: "You cannot add the same dependency twice.",
type: "error", type: "error",
@@ -1051,8 +1050,8 @@ export default defineNuxtComponent({
this.newDependencyId = ""; this.newDependencyId = "";
} catch { } catch {
if (!hideErrors) { if (!hideErrors) {
const { addNotification } = injectNotificationManager(); this.$notify({
addNotification({ group: "main",
title: "Invalid Dependency", title: "Invalid Dependency",
text: "The specified dependency could not be found", text: "The specified dependency could not be found",
type: "error", type: "error",
@@ -1145,8 +1144,8 @@ export default defineNuxtComponent({
)}`, )}`,
); );
} catch (err) { } catch (err) {
const { addNotification } = injectNotificationManager(); this.$notify({
addNotification({ group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -1170,8 +1169,8 @@ export default defineNuxtComponent({
try { try {
await this.createVersionRaw(this.version); await this.createVersionRaw(this.version);
} catch (err) { } catch (err) {
const { addNotification } = injectNotificationManager(); this.$notify({
addNotification({ group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -1294,15 +1293,15 @@ export default defineNuxtComponent({
this.$refs.modal_package_mod.hide(); this.$refs.modal_package_mod.hide();
const { addNotification } = injectNotificationManager(); this.$notify({
addNotification({ group: "main",
title: "Packaging Success", title: "Packaging Success",
text: "Your data pack was successfully packaged as a mod! Make sure to playtest to check for errors.", text: "Your data pack was successfully packaged as a mod! Make sure to playtest to check for errors.",
type: "success", type: "success",
}); });
} catch (err) { } catch (err) {
const { addNotification } = injectNotificationManager(); this.$notify({
addNotification({ group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
+10 -13
View File
@@ -251,31 +251,28 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import {
CheckIcon,
CurrencyIcon,
ExternalIcon,
ModrinthPlusIcon,
ServerIcon,
UserIcon,
XIcon,
} from "@modrinth/assets";
import { import {
Avatar, Avatar,
ButtonStyled, ButtonStyled,
CopyCode, CopyCode,
DropdownSelect, DropdownSelect,
injectNotificationManager,
NewModal, NewModal,
Toggle, Toggle,
useRelativeTime, useRelativeTime,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { formatCategory, formatPrice } from "@modrinth/utils"; import { formatCategory, formatPrice } from "@modrinth/utils";
import {
CheckIcon,
XIcon,
UserIcon,
ModrinthPlusIcon,
ServerIcon,
ExternalIcon,
CurrencyIcon,
} from "@modrinth/assets";
import dayjs from "dayjs"; import dayjs from "dayjs";
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
import { products } from "~/generated/state.json"; import { products } from "~/generated/state.json";
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
const { addNotification } = injectNotificationManager();
const route = useRoute(); const route = useRoute();
const vintl = useVIntl(); const vintl = useVIntl();
@@ -258,31 +258,31 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { EditIcon, PlusIcon, SaveIcon, SettingsIcon, TrashIcon, XIcon } from "@modrinth/assets";
import { import {
ButtonStyled,
commonMessages,
CopyCode, CopyCode,
injectNotificationManager,
NewModal,
ServerNotice,
TagItem, TagItem,
ButtonStyled,
ServerNotice,
commonMessages,
NewModal,
TeleportDropdownMenu, TeleportDropdownMenu,
Toggle, Toggle,
useRelativeTime, useRelativeTime,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts"; import { SettingsIcon, PlusIcon, SaveIcon, TrashIcon, EditIcon, XIcon } from "@modrinth/assets";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { useVIntl } from "@vintl/vintl";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useVIntl } from "@vintl/vintl";
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
import { computed } from "vue"; import { computed } from "vue";
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue"; import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts"; import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
const formatRelativeTime = useRelativeTime(); const formatRelativeTime = useRelativeTime();
const app = useNuxtApp() as unknown as { $notify: any };
const notices = ref<ServerNoticeType[]>([]); const notices = ref<ServerNoticeType[]>([]);
const createNoticeModal = ref<InstanceType<typeof NewModal>>(); const createNoticeModal = ref<InstanceType<typeof NewModal>>();
const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>(); const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>();
@@ -351,13 +351,15 @@ async function deleteNotice(notice: ServerNoticeType) {
method: "DELETE", method: "DELETE",
}) })
.then(() => { .then(() => {
addNotification({ app.$notify({
group: "main",
title: `Successfully deleted notice #${notice.id}`, title: `Successfully deleted notice #${notice.id}`,
type: "success", type: "success",
}); });
}) })
.catch((err) => { .catch((err) => {
addNotification({ app.$notify({
group: "main",
title: "Error deleting notice", title: "Error deleting notice",
text: err, text: err,
type: "error", type: "error",
@@ -384,6 +386,7 @@ const noticeSubmitError = computed(() => {
function validateSubmission(message: string) { function validateSubmission(message: string) {
if (noticeSubmitError.value) { if (noticeSubmitError.value) {
addNotification({ addNotification({
group: "main",
title: message, title: message,
text: noticeSubmitError.value, text: noticeSubmitError.value,
type: "error", type: "error",
@@ -413,7 +416,8 @@ async function saveChanges() {
: undefined, : undefined,
}, },
}).catch((err) => { }).catch((err) => {
addNotification({ app.$notify({
group: "main",
title: "Error saving changes to notice", title: "Error saving changes to notice",
text: err, text: err,
type: "error", type: "error",
@@ -443,7 +447,8 @@ async function createNotice() {
: undefined, : undefined,
}, },
}).catch((err) => { }).catch((err) => {
addNotification({ app.$notify({
group: "main",
title: "Error creating notice", title: "Error creating notice",
text: err, text: err,
type: "error", type: "error",
+2 -3
View File
@@ -32,10 +32,8 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import { MailIcon } from "@modrinth/assets"; import { MailIcon } from "@modrinth/assets";
import { ButtonStyled, injectNotificationManager } from "@modrinth/ui";
const { addNotification } = injectNotificationManager();
const userEmail = ref(""); const userEmail = ref("");
@@ -52,6 +50,7 @@ async function getUserFromEmail() {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
addNotification({ addNotification({
group: "main",
title: "An error occurred", title: "An error occurred",
text: err.data.description, text: err.data.description,
type: "error", type: "error",
+9 -6
View File
@@ -80,14 +80,13 @@
</template> </template>
<script setup> <script setup>
import { CheckIcon, XIcon } from "@modrinth/assets"; import { Button, Avatar, commonMessages } from "@modrinth/ui";
import { Avatar, Button, commonMessages, injectNotificationManager } from "@modrinth/ui"; import { XIcon, CheckIcon } from "@modrinth/assets";
import { useAuth } from "@/composables/auth.js";
import { useBaseFetch } from "@/composables/fetch.js"; import { useBaseFetch } from "@/composables/fetch.js";
import { useAuth } from "@/composables/auth.js";
import { useScopes } from "@/composables/auth/scopes.ts"; import { useScopes } from "@/composables/auth/scopes.ts";
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
const messages = defineMessages({ const messages = defineMessages({
@@ -118,6 +117,8 @@ const messages = defineMessages({
}, },
}); });
const data = useNuxtApp();
const router = useNativeRoute(); const router = useNativeRoute();
const auth = await useAuth(); const auth = await useAuth();
const { scopesToDefinitions } = useScopes(); const { scopesToDefinitions } = useScopes();
@@ -195,7 +196,8 @@ const onAuthorize = async () => {
throw new Error(formatMessage(messages.noRedirectUrlError)); throw new Error(formatMessage(messages.noRedirectUrlError));
} catch { } catch {
addNotification({ data.$notify({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -221,7 +223,8 @@ const onReject = async () => {
throw new Error(formatMessage(messages.noRedirectUrlError)); throw new Error(formatMessage(messages.noRedirectUrlError));
} catch { } catch {
addNotification({ data.$notify({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -67,11 +67,10 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { KeyIcon, MailIcon, SendIcon } from "@modrinth/assets"; import { SendIcon, MailIcon, KeyIcon } from "@modrinth/assets";
import { commonMessages, injectNotificationManager } from "@modrinth/ui"; import { commonMessages } from "@modrinth/ui";
import HCaptcha from "@/components/ui/HCaptcha.vue"; import HCaptcha from "@/components/ui/HCaptcha.vue";
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
const methodChoiceMessages = defineMessages({ const methodChoiceMessages = defineMessages({
@@ -180,12 +179,14 @@ async function recovery() {
}); });
addNotification({ addNotification({
group: "main",
title: formatMessage(emailSentNotificationMessages.title), title: formatMessage(emailSentNotificationMessages.title),
text: formatMessage(emailSentNotificationMessages.text), text: formatMessage(emailSentNotificationMessages.text),
type: "success", type: "success",
}); });
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -210,6 +211,7 @@ async function changePassword() {
}); });
addNotification({ addNotification({
group: "main",
title: formatMessage(passwordResetNotificationMessages.title), title: formatMessage(passwordResetNotificationMessages.title),
text: formatMessage(passwordResetNotificationMessages.text), text: formatMessage(passwordResetNotificationMessages.text),
type: "success", type: "success",
@@ -217,6 +219,7 @@ async function changePassword() {
await navigateTo("/auth/sign-in"); await navigateTo("/auth/sign-in");
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
+8 -7
View File
@@ -130,20 +130,19 @@
<script setup> <script setup>
import { import {
KeyIcon,
MailIcon,
RightArrowIcon, RightArrowIcon,
SSODiscordIcon,
SSOGitHubIcon, SSOGitHubIcon,
SSOGitLabIcon,
SSOGoogleIcon,
SSOMicrosoftIcon, SSOMicrosoftIcon,
SSOSteamIcon, SSOSteamIcon,
SSOGoogleIcon,
SSODiscordIcon,
SSOGitLabIcon,
KeyIcon,
MailIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { commonMessages, injectNotificationManager } from "@modrinth/ui"; import { commonMessages } from "@modrinth/ui";
import HCaptcha from "@/components/ui/HCaptcha.vue"; import HCaptcha from "@/components/ui/HCaptcha.vue";
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
const messages = defineMessages({ const messages = defineMessages({
@@ -233,6 +232,7 @@ async function beginPasswordSignIn() {
} }
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
@@ -257,6 +257,7 @@ async function begin2FASignIn() {
await finishSignIn(res.session); await finishSignIn(res.session);
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
+10 -9
View File
@@ -134,21 +134,20 @@
<script setup> <script setup>
import { import {
RightArrowIcon,
UserIcon,
SSOGitHubIcon,
SSOMicrosoftIcon,
SSOGoogleIcon,
SSOSteamIcon,
SSODiscordIcon,
KeyIcon, KeyIcon,
MailIcon, MailIcon,
RightArrowIcon,
SSODiscordIcon,
SSOGitHubIcon,
SSOGitLabIcon, SSOGitLabIcon,
SSOGoogleIcon,
SSOMicrosoftIcon,
SSOSteamIcon,
UserIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Checkbox, commonMessages, injectNotificationManager } from "@modrinth/ui"; import { Checkbox, commonMessages } from "@modrinth/ui";
import HCaptcha from "@/components/ui/HCaptcha.vue"; import HCaptcha from "@/components/ui/HCaptcha.vue";
const { addNotification } = injectNotificationManager();
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
const messages = defineMessages({ const messages = defineMessages({
@@ -226,6 +225,7 @@ async function createAccount() {
try { try {
if (confirmPassword.value !== password.value) { if (confirmPassword.value !== password.value) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: formatMessage({ text: formatMessage({
id: "auth.sign-up.notification.password-mismatch.text", id: "auth.sign-up.notification.password-mismatch.text",
@@ -262,6 +262,7 @@ async function createAccount() {
} }
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
+6 -4
View File
@@ -367,7 +367,6 @@ import {
BoxIcon, BoxIcon,
CalendarIcon, CalendarIcon,
EditIcon, EditIcon,
GlobeIcon,
GridIcon, GridIcon,
ImageIcon, ImageIcon,
LibraryIcon, LibraryIcon,
@@ -379,6 +378,7 @@ import {
UpdatedIcon, UpdatedIcon,
UploadIcon, UploadIcon,
XIcon, XIcon,
GlobeIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
Avatar, Avatar,
@@ -387,17 +387,17 @@ import {
ConfirmModal, ConfirmModal,
DropdownSelect, DropdownSelect,
FileInput, FileInput,
injectNotificationManager,
PopoutMenu, PopoutMenu,
useRelativeTime, useRelativeTime,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { isAdmin } from "@modrinth/utils"; import { isAdmin } from "@modrinth/utils";
import UpToDate from "assets/images/illustrations/up_to_date.svg"; import UpToDate from "assets/images/illustrations/up_to_date.svg";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import { addNotification } from "~/composables/notifs.js";
import NavRow from "~/components/ui/NavRow.vue"; import NavRow from "~/components/ui/NavRow.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
const { addNotification } = injectNotificationManager();
const vintl = useVIntl(); const vintl = useVIntl();
const { formatMessage } = vintl; const { formatMessage } = vintl;
const formatRelativeTime = useRelativeTime(); const formatRelativeTime = useRelativeTime();
@@ -664,6 +664,7 @@ async function saveChanges() {
isEditing.value = false; isEditing.value = false;
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err, text: err,
type: "error", type: "error",
@@ -687,6 +688,7 @@ async function deleteCollection() {
} }
} catch (err) { } catch (err) {
addNotification({ addNotification({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: "error", type: "error",
+1 -4
View File
@@ -99,10 +99,7 @@
import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets"; import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets";
import { Avatar } from "@modrinth/ui"; import { Avatar } from "@modrinth/ui";
import NotificationItem from "~/components/ui/NotificationItem.vue"; import NotificationItem from "~/components/ui/NotificationItem.vue";
import { import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts";
fetchExtraNotificationData,
groupNotifications,
} from "~/helpers/platform-notifications.ts";
useHead({ useHead({
title: "Dashboard - Modrinth", title: "Dashboard - Modrinth",
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff. Show more