1
1
<script setup>
2
- import { computed , onMounted , onUnmounted , ref , watch , provide } from ' vue'
3
- import { RouterView , useRoute , useRouter } from ' vue-router'
2
+ import ModrinthAppLogo from ' @/assets/modrinth_app.svg?component'
3
+ import ModrinthLoadingIndicator from ' @/components/LoadingIndicatorBar.vue'
4
+ import AccountsCard from ' @/components/ui/AccountsCard.vue'
5
+ import Breadcrumbs from ' @/components/ui/Breadcrumbs.vue'
6
+ import ErrorModal from ' @/components/ui/ErrorModal.vue'
7
+ import FriendsList from ' @/components/ui/friends/FriendsList.vue'
8
+ import IncompatibilityWarningModal from ' @/components/ui/install_flow/IncompatibilityWarningModal.vue'
9
+ import InstallConfirmModal from ' @/components/ui/install_flow/InstallConfirmModal.vue'
10
+ import ModInstallModal from ' @/components/ui/install_flow/ModInstallModal.vue'
11
+ import InstanceCreationModal from ' @/components/ui/InstanceCreationModal.vue'
12
+ import AppSettingsModal from ' @/components/ui/modal/AppSettingsModal.vue'
13
+ import AuthGrantFlowWaitModal from ' @/components/ui/modal/AuthGrantFlowWaitModal.vue'
14
+ import NavButton from ' @/components/ui/NavButton.vue'
15
+ import PromotionWrapper from ' @/components/ui/PromotionWrapper.vue'
16
+ import QuickInstanceSwitcher from ' @/components/ui/QuickInstanceSwitcher.vue'
17
+ import RunningAppBar from ' @/components/ui/RunningAppBar.vue'
18
+ import SplashScreen from ' @/components/ui/SplashScreen.vue'
19
+ import URLConfirmModal from ' @/components/ui/URLConfirmModal.vue'
20
+ import { useCheckDisableMouseover } from ' @/composables/macCssFix.js'
21
+ import { hide_ads_window , init_ads_window , show_ads_window } from ' @/helpers/ads.js'
22
+ import { debugAnalytics , initAnalytics , optOutAnalytics , trackEvent } from ' @/helpers/analytics'
23
+ import { get_user } from ' @/helpers/cache.js'
24
+ import { command_listener , warning_listener } from ' @/helpers/events.js'
25
+ import { useFetch } from ' @/helpers/fetch.js'
26
+ import { cancelLogin , get as getCreds , login , logout } from ' @/helpers/mr_auth.js'
27
+ import { list } from ' @/helpers/profile.js'
28
+ import { get } from ' @/helpers/settings.ts'
29
+ import { get_opening_command , initialize_state } from ' @/helpers/state'
30
+ import { getOS , isDev , restartApp } from ' @/helpers/utils.js'
31
+ import { useError } from ' @/store/error.js'
32
+ import { useInstall } from ' @/store/install.js'
33
+ import { useLoading , useTheming } from ' @/store/state'
4
34
import {
5
35
ArrowBigUpDashIcon ,
6
36
ChangeSkinIcon ,
@@ -13,69 +43,48 @@ import {
13
43
LogOutIcon ,
14
44
MaximizeIcon ,
15
45
MinimizeIcon ,
46
+ NewspaperIcon ,
47
+ NotepadTextIcon ,
16
48
PlusIcon ,
17
49
RestoreIcon ,
18
50
RightArrowIcon ,
19
51
SettingsIcon ,
20
52
WorldIcon ,
21
53
XIcon ,
22
- NewspaperIcon ,
23
54
} from ' @modrinth/assets'
24
55
import {
25
56
Avatar ,
26
57
Button ,
27
58
ButtonStyled ,
28
- Notifications ,
29
- OverflowMenu ,
30
59
NewsArticleCard ,
60
+ NotificationPanel ,
61
+ OverflowMenu ,
62
+ provideNotificationManager ,
31
63
} from ' @modrinth/ui'
32
- import { useLoading , useTheming } from ' @/store/state'
33
- import ModrinthAppLogo from ' @/assets/modrinth_app.svg?component'
34
- import AccountsCard from ' @/components/ui/AccountsCard.vue'
35
- import InstanceCreationModal from ' @/components/ui/InstanceCreationModal.vue'
36
- import { get } from ' @/helpers/settings.ts'
37
- import Breadcrumbs from ' @/components/ui/Breadcrumbs.vue'
38
- import RunningAppBar from ' @/components/ui/RunningAppBar.vue'
39
- import SplashScreen from ' @/components/ui/SplashScreen.vue'
40
- import ErrorModal from ' @/components/ui/ErrorModal.vue'
41
- import ModrinthLoadingIndicator from ' @/components/LoadingIndicatorBar.vue'
42
- import { handleError , useNotifications } from ' @/store/notifications.js'
43
- import { command_listener , warning_listener } from ' @/helpers/events.js'
44
- import { type } from ' @tauri-apps/plugin-os'
45
- import { getOS , isDev , restartApp } from ' @/helpers/utils.js'
46
- import { debugAnalytics , initAnalytics , optOutAnalytics , trackEvent } from ' @/helpers/analytics'
47
- import { getCurrentWindow } from ' @tauri-apps/api/window'
64
+ import { renderString } from ' @modrinth/utils'
48
65
import { getVersion } from ' @tauri-apps/api/app'
49
- import URLConfirmModal from ' @/components/ui/URLConfirmModal.vue'
50
- import { create_profile_and_install_from_file } from ' ./helpers/pack'
51
- import { useError } from ' @/store/error.js'
52
- import { useCheckDisableMouseover } from ' @/composables/macCssFix.js'
53
- import ModInstallModal from ' @/components/ui/install_flow/ModInstallModal.vue'
54
- import IncompatibilityWarningModal from ' @/components/ui/install_flow/IncompatibilityWarningModal.vue'
55
- import InstallConfirmModal from ' @/components/ui/install_flow/InstallConfirmModal.vue'
56
- import { useInstall } from ' @/store/install.js'
57
66
import { invoke } from ' @tauri-apps/api/core'
58
- import { get_opening_command , initialize_state } from ' @/helpers/state'
59
- import { saveWindowState , StateFlags } from ' @tauri-apps/plugin-window-state'
60
- import { renderString } from ' @modrinth/utils'
61
- import { useFetch } from ' @/helpers/fetch.js'
62
- import { check } from ' @tauri-apps/plugin-updater'
63
- import NavButton from ' @/components/ui/NavButton.vue'
64
- import { cancelLogin , get as getCreds , login , logout } from ' @/helpers/mr_auth.js'
65
- import { get_user } from ' @/helpers/cache.js'
66
- import AppSettingsModal from ' @/components/ui/modal/AppSettingsModal.vue'
67
- import AuthGrantFlowWaitModal from ' @/components/ui/modal/AuthGrantFlowWaitModal.vue'
68
- import PromotionWrapper from ' @/components/ui/PromotionWrapper.vue'
69
- import { hide_ads_window , init_ads_window } from ' @/helpers/ads.js'
70
- import FriendsList from ' @/components/ui/friends/FriendsList.vue'
67
+ import { getCurrentWindow } from ' @tauri-apps/api/window'
71
68
import { openUrl } from ' @tauri-apps/plugin-opener'
72
- import QuickInstanceSwitcher from ' @/components/ui/QuickInstanceSwitcher.vue'
73
- import { get_available_capes , get_available_skins } from ' ./helpers/skins'
69
+ import { type } from ' @tauri-apps/plugin-os'
70
+ import { check } from ' @tauri-apps/plugin-updater'
71
+ import { saveWindowState , StateFlags } from ' @tauri-apps/plugin-window-state'
72
+ import { $fetch } from ' ofetch'
73
+ import { computed , onMounted , onUnmounted , provide , ref , watch } from ' vue'
74
+ import { RouterView , useRoute , useRouter } from ' vue-router'
75
+ import { create_profile_and_install_from_file } from ' ./helpers/pack'
74
76
import { generateSkinPreviews } from ' ./helpers/rendering/batch-skin-renderer'
77
+ import { get_available_capes , get_available_skins } from ' ./helpers/skins'
78
+ import { AppNotificationManager } from ' ./providers/app-notifications'
75
79
76
80
const themeStore = useTheming ()
77
81
82
+ const notificationManager = new AppNotificationManager ()
83
+ provideNotificationManager (notificationManager)
84
+ const { handleError , addNotification } = notificationManager
85
+
78
86
const news = ref ([])
87
+ const availableSurvey = ref (false )
79
88
80
89
const urlModal = ref (null )
81
90
@@ -167,7 +176,7 @@ async function setupApp() {
167
176
}
168
177
169
178
await warning_listener ((e ) =>
170
- notificationsWrapper . value . addNotification ({
179
+ addNotification ({
171
180
title: ' Warning' ,
172
181
text: e .message ,
173
182
type: ' warn' ,
@@ -220,6 +229,12 @@ async function setupApp() {
220
229
} catch (error) {
221
230
console .warn (' Failed to generate skin previews in app setup.' , error)
222
231
}
232
+
233
+ if (osType === ' windows' ) {
234
+ await processPendingSurveys ()
235
+ } else {
236
+ console .info (' Skipping user surveys on non-Windows platforms' )
237
+ }
223
238
}
224
239
225
240
const stateFailed = ref (false )
@@ -251,9 +266,6 @@ const route = useRoute()
251
266
const loading = useLoading ()
252
267
loading .setEnabled (false )
253
268
254
- const notifications = useNotifications ()
255
- const notificationsWrapper = ref ()
256
-
257
269
const error = useError ()
258
270
const errorModal = ref ()
259
271
@@ -335,8 +347,6 @@ watch(
335
347
onMounted (() => {
336
348
invoke (' show_window' )
337
349
338
- notifications .setNotifs (notificationsWrapper .value )
339
-
340
350
error .setErrorModal (errorModal .value )
341
351
342
352
install .setIncompatibilityWarningModal (incompatibilityWarningModal)
@@ -412,6 +422,116 @@ function handleAuxClick(e) {
412
422
e .target .dispatchEvent (event )
413
423
}
414
424
}
425
+
426
+ function cleanupOldSurveyDisplayData () {
427
+ const threeWeeksAgo = new Date ()
428
+ threeWeeksAgo .setDate (threeWeeksAgo .getDate () - 21 )
429
+
430
+ for (let i = 0 ; i < localStorage .length ; i++ ) {
431
+ const key = localStorage .key (i)
432
+
433
+ if (key .startsWith (' survey-' ) && key .endsWith (' -display' )) {
434
+ const dateValue = new Date (localStorage .getItem (key))
435
+ if (dateValue < threeWeeksAgo) {
436
+ localStorage .removeItem (key)
437
+ }
438
+ }
439
+ }
440
+ }
441
+
442
+ async function openSurvey () {
443
+ if (! availableSurvey .value ) {
444
+ console .error (' No survey to open' )
445
+ return
446
+ }
447
+
448
+ const creds = await getCreds ().catch (handleError)
449
+ const userId = creds? .user_id
450
+
451
+ const formId = availableSurvey .value .tally_id
452
+
453
+ const popupOptions = {
454
+ layout: ' modal' ,
455
+ width: 700 ,
456
+ autoClose: 2000 ,
457
+ hideTitle: true ,
458
+ hiddenFields: {
459
+ user_id: userId,
460
+ },
461
+ onOpen : () => console .info (' Opened user survey' ),
462
+ onClose : () => {
463
+ console .info (' Closed user survey' )
464
+ show_ads_window ()
465
+ },
466
+ onSubmit : () => console .info (' Active user survey submitted' ),
467
+ }
468
+
469
+ try {
470
+ hide_ads_window ()
471
+ if (window .Tally ? .openPopup ) {
472
+ console .info (` Opening Tally popup for user survey (form ID: ${ formId} )` )
473
+ dismissSurvey ()
474
+ window .Tally .openPopup (formId, popupOptions)
475
+ } else {
476
+ console .warn (' Tally script not yet loaded' )
477
+ show_ads_window ()
478
+ }
479
+ } catch (e) {
480
+ console .error (' Error opening Tally popup:' , e)
481
+ show_ads_window ()
482
+ }
483
+
484
+ console .info (` Found user survey to show with tally_id: ${ formId} ` )
485
+ window .Tally .openPopup (formId, popupOptions)
486
+ }
487
+
488
+ function dismissSurvey () {
489
+ localStorage .setItem (` survey-${ availableSurvey .value .id } -display` , new Date ())
490
+ availableSurvey .value = undefined
491
+ }
492
+
493
+ async function processPendingSurveys () {
494
+ function isWithinLastTwoWeeks (date ) {
495
+ const twoWeeksAgo = new Date ()
496
+ twoWeeksAgo .setDate (twoWeeksAgo .getDate () - 14 )
497
+ return date >= twoWeeksAgo
498
+ }
499
+
500
+ cleanupOldSurveyDisplayData ()
501
+
502
+ const creds = await getCreds ().catch (handleError)
503
+ const userId = creds? .user_id
504
+
505
+ const instances = await list ().catch (handleError)
506
+ const isActivePlayer =
507
+ instances .findIndex (
508
+ (instance ) =>
509
+ isWithinLastTwoWeeks (instance .last_played ) && ! isWithinLastTwoWeeks (instance .created ),
510
+ ) >= 0
511
+
512
+ let surveys = []
513
+ try {
514
+ surveys = await $fetch (' https://api.modrinth.com/v2/surveys' )
515
+ } catch (e) {
516
+ console .error (' Error fetching surveys:' , e)
517
+ }
518
+
519
+ const surveyToShow = surveys .find (
520
+ (survey ) =>
521
+ !! (
522
+ localStorage .getItem (` survey-${ survey .id } -display` ) === null &&
523
+ survey .type === ' tally_app' &&
524
+ ((survey .condition === ' active_player' && isActivePlayer) ||
525
+ (survey .assigned_users ? .includes (userId) && ! survey .dismissed_users ? .includes (userId)))
526
+ ),
527
+ )
528
+
529
+ if (surveyToShow) {
530
+ availableSurvey .value = surveyToShow
531
+ } else {
532
+ console .info (' No user survey to show' )
533
+ }
534
+ }
415
535
< / script>
416
536
417
537
< template>
@@ -565,6 +685,28 @@ function handleAuxClick(e) {
565
685
: class = " { 'sidebar-enabled': sidebarVisible }"
566
686
>
567
687
< div class = " app-viewport flex-grow router-view" >
688
+ < transition name= " popup-survey" >
689
+ < div
690
+ v- if = " availableSurvey"
691
+ class = " w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-divider border-[1px] border-solid border-b-0 p-4"
692
+ >
693
+ < h2 class = " text-lg font-extrabold mt-0 mb-2" > Hey there Modrinth user! < / h2>
694
+ < p class = " m-0 leading-tight" >
695
+ Would you mind answering a few questions about your experience with Modrinth App?
696
+ < / p>
697
+ < p class = " mt-3 mb-4 leading-tight" >
698
+ This feedback will go directly to the Modrinth team and help guide future updates!
699
+ < / p>
700
+ < div class = " flex gap-2" >
701
+ < ButtonStyled color= " brand" >
702
+ < button @click= " openSurvey" >< NotepadTextIcon / > Take survey< / button>
703
+ < / ButtonStyled>
704
+ < ButtonStyled>
705
+ < button @click= " dismissSurvey" >< XIcon / > No thanks< / button>
706
+ < / ButtonStyled>
707
+ < / div>
708
+ < / div>
709
+ < / transition>
568
710
< div
569
711
class = " loading-indicator-container h-8 fixed z-50"
570
712
: style= " {
@@ -657,7 +799,7 @@ function handleAuxClick(e) {
657
799
< / div>
658
800
< / div>
659
801
< URLConfirmModal ref= " urlModal" / >
660
- < Notifications ref = " notificationsWrapper " sidebar / >
802
+ < NotificationPanel has - sidebar / >
661
803
< ErrorModal ref= " errorModal" / >
662
804
< ModInstallModal ref= " modInstallModal" / >
663
805
< IncompatibilityWarningModal ref= " incompatibilityWarningModal" / >
@@ -862,6 +1004,26 @@ function handleAuxClick(e) {
862
1004
.sidebar - teleport- content: empty + .sidebar - default- content .sidebar - enabled {
863
1005
display: contents;
864
1006
}
1007
+
1008
+ .popup - survey- enter- active {
1009
+ transition:
1010
+ opacity 0 .25s ease,
1011
+ transform 0 .25s cubic- bezier (0.51 , 1.08 , 0.35 , 1.15 );
1012
+ transform- origin: top center;
1013
+ }
1014
+
1015
+ .popup - survey- leave- active {
1016
+ transition:
1017
+ opacity 0 .25s ease,
1018
+ transform 0 .25s cubic- bezier (0.68 , - 0.17 , 0.23 , 0.11 );
1019
+ transform- origin: top center;
1020
+ }
1021
+
1022
+ .popup - survey- enter- from,
1023
+ .popup - survey- leave- to {
1024
+ opacity: 0 ;
1025
+ transform: translateY (10rem ) scale (0.8 ) scaleY (1.6 );
1026
+ }
865
1027
< / style>
866
1028
< style>
867
1029
.mac {
0 commit comments