feat: add unknown .mrpack install warning modal (#5942)
* Update modpack button copy * Change outlined button style for standard buttons * add unknown pack warning modal * implementation * Redo download toasts * prepr * improve hit area of window controls * implement "don't show again" * prepr * duplicate modal ref declarations * increase spacing of progress items * address truman review
This commit is contained in:
@@ -108,6 +108,7 @@ pub struct CreatePackProfile {
|
||||
pub icon: Option<PathBuf>, // the icon for the profile
|
||||
pub icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
|
||||
pub linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
|
||||
pub unknown_file: bool, // true when pack file isn't found on Modrinth via hash lookup
|
||||
pub skip_install_profile: Option<bool>,
|
||||
pub no_watch: Option<bool>,
|
||||
}
|
||||
@@ -123,6 +124,7 @@ impl Default for CreatePackProfile {
|
||||
icon: None,
|
||||
icon_url: None,
|
||||
linked_data: None,
|
||||
unknown_file: false,
|
||||
skip_install_profile: Some(true),
|
||||
no_watch: Some(false),
|
||||
}
|
||||
@@ -145,16 +147,16 @@ pub struct CreatePackDescription {
|
||||
pub profile_path: String,
|
||||
}
|
||||
|
||||
pub fn get_profile_from_pack(
|
||||
pub async fn get_profile_from_pack(
|
||||
location: CreatePackLocation,
|
||||
) -> CreatePackProfile {
|
||||
) -> crate::Result<CreatePackProfile> {
|
||||
match location {
|
||||
CreatePackLocation::FromVersionId {
|
||||
project_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
} => CreatePackProfile {
|
||||
} => Ok(CreatePackProfile {
|
||||
name: title,
|
||||
icon_url,
|
||||
linked_data: Some(LinkedData {
|
||||
@@ -163,7 +165,7 @@ pub fn get_profile_from_pack(
|
||||
locked: true,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
}),
|
||||
CreatePackLocation::FromFile { path } => {
|
||||
let file_name = path
|
||||
.file_stem()
|
||||
@@ -171,10 +173,35 @@ pub fn get_profile_from_pack(
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
CreatePackProfile {
|
||||
let state = State::get().await?;
|
||||
let file_bytes = io::read(&path).await?;
|
||||
let hash =
|
||||
crate::util::fetch::sha1_async(bytes::Bytes::from(file_bytes))
|
||||
.await?;
|
||||
let is_known_file = match CachedEntry::get_file_many(
|
||||
&[&hash],
|
||||
Some(CacheBehaviour::StaleWhileRevalidateSkipOffline),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(files) => !files.is_empty(),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to check Modrinth file hash for {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
Ok(CreatePackProfile {
|
||||
name: file_name,
|
||||
unknown_file: !is_known_file,
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ pub enum FeatureFlag {
|
||||
WorldsTab,
|
||||
WorldsInHome,
|
||||
ServerRamAsBytesAlwaysOn,
|
||||
AlwaysShowAppControls,
|
||||
SkipUnknownPackWarning,
|
||||
ServersInApp,
|
||||
ServerProjectQa,
|
||||
I18nDebug,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
|
||||
export type IconComponent = FunctionalComponent<SVGAttributes>
|
||||
|
||||
import _AffiliateIcon from './icons/affiliate.svg?component'
|
||||
import _AlignLeftIcon from './icons/align-left.svg?component'
|
||||
import _ArchiveIcon from './icons/archive.svg?component'
|
||||
@@ -51,6 +53,7 @@ import _ChevronLeftIcon from './icons/chevron-left.svg?component'
|
||||
import _ChevronRightIcon from './icons/chevron-right.svg?component'
|
||||
import _ChevronUpIcon from './icons/chevron-up.svg?component'
|
||||
import _CircleAlertIcon from './icons/circle-alert.svg?component'
|
||||
import _CircleArrowRightIcon from './icons/circle-arrow-right.svg?component'
|
||||
import _CircleUserIcon from './icons/circle-user.svg?component'
|
||||
import _ClearIcon from './icons/clear.svg?component'
|
||||
import _ClientIcon from './icons/client.svg?component'
|
||||
@@ -392,8 +395,6 @@ import _XCircleIcon from './icons/x-circle.svg?component'
|
||||
import _ZoomInIcon from './icons/zoom-in.svg?component'
|
||||
import _ZoomOutIcon from './icons/zoom-out.svg?component'
|
||||
|
||||
export type IconComponent = FunctionalComponent<SVGAttributes>
|
||||
|
||||
export const AffiliateIcon = _AffiliateIcon
|
||||
export const AlignLeftIcon = _AlignLeftIcon
|
||||
export const ArchiveIcon = _ArchiveIcon
|
||||
@@ -442,6 +443,7 @@ export const ChevronLeftIcon = _ChevronLeftIcon
|
||||
export const ChevronRightIcon = _ChevronRightIcon
|
||||
export const ChevronUpIcon = _ChevronUpIcon
|
||||
export const CircleAlertIcon = _CircleAlertIcon
|
||||
export const CircleArrowRightIcon = _CircleArrowRightIcon
|
||||
export const CircleUserIcon = _CircleUserIcon
|
||||
export const ClearIcon = _ClearIcon
|
||||
export const ClientIcon = _ClientIcon
|
||||
|
||||
17
packages/assets/icons/circle-arrow-right.svg
Normal file
17
packages/assets/icons/circle-arrow-right.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- @license lucide-static v0.562.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-circle-arrow-right"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="m12 16 4-4-4-4" />
|
||||
<path d="M8 12h8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -204,9 +204,6 @@ const colorVariables = computed(() => {
|
||||
|
||||
if (props.type === 'outlined' || props.type === 'transparent') {
|
||||
colors.bg = 'transparent'
|
||||
if (props.hoverColorFill === 'none') {
|
||||
hoverColors.bg = 'transparent'
|
||||
}
|
||||
colors = setColorFill(colors, props.colorFill === 'auto' ? 'text' : props.colorFill)
|
||||
hoverColors = setColorFill(
|
||||
hoverColors,
|
||||
@@ -245,7 +242,7 @@ const fontSize = computed(() => {
|
||||
<div
|
||||
class="btn-wrapper"
|
||||
:class="[{ outline: type === 'outlined', chip: type === 'chip' }, fontSize]"
|
||||
:style="`${colorVariables}--_height:${height};--_width:${width};--_radius: ${radius};--_padding-x:${paddingX};--_padding-y:${paddingY};--_gap:${gap};--_font-weight:${fontWeight};--_icon-size:${iconSize};`"
|
||||
:style="`${colorVariables}--_height:${height};--_width:${width};--_radius: ${radius};--_padding-x:${paddingX};--_padding-y:${paddingY};--_gap:${gap};--_font-weight:${fontWeight};--_icon-size:${iconSize};--_outline-color:${color === 'standard' && type === 'outlined' ? 'var(--surface-5)' : 'currentColor'}`"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -266,10 +263,8 @@ const fontSize = computed(() => {
|
||||
> *:first-child
|
||||
> *:first-child
|
||||
> :is(button, a, .button-like):first-child {
|
||||
@apply flex cursor-pointer flex-row items-center justify-center border-solid border border-transparent bg-[--_bg] text-[--_text] h-[--_height] min-w-[--_width] rounded-[--_radius] px-[--_padding-x] py-[--_padding-y] gap-[--_gap] font-[--_font-weight] whitespace-nowrap;
|
||||
@apply flex cursor-pointer flex-row items-center justify-center border-solid border-2 border-transparent bg-[--_bg] text-[--_text] h-[--_height] min-w-[--_width] rounded-[--_radius] px-[--_padding-x] py-[--_padding-y] gap-[--_gap] font-[--_font-weight] whitespace-nowrap;
|
||||
box-shadow: var(--_box-shadow, inset 0 0 0 transparent);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
will-change: filter;
|
||||
transition:
|
||||
scale 0.125s ease-in-out,
|
||||
background-color 0.25s ease-in-out,
|
||||
@@ -328,7 +323,7 @@ const fontSize = computed(() => {
|
||||
> *:first-child
|
||||
> *:first-child
|
||||
> :is(button, a, .button-like):first-child {
|
||||
@apply border-current;
|
||||
@apply border-[--_outline-color,currentColor];
|
||||
}
|
||||
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
|
||||
@@ -95,11 +95,11 @@ const messages = defineMessages({
|
||||
},
|
||||
modpackBaseTitle: {
|
||||
id: 'creation-flow.modal.setup-type.option.modpack-base.title',
|
||||
defaultMessage: 'Modpack base',
|
||||
defaultMessage: 'Install modpack',
|
||||
},
|
||||
modpackBaseDescription: {
|
||||
id: 'creation-flow.modal.setup-type.option.modpack-base.description',
|
||||
defaultMessage: 'Use a popular modpack or upload one as your starting point.',
|
||||
defaultMessage: 'Browse modpacks on Modrinth or import one from a file.',
|
||||
},
|
||||
importInstanceTitle: {
|
||||
id: 'creation-flow.modal.setup-type.option.import-instance.title',
|
||||
|
||||
@@ -24,11 +24,15 @@
|
||||
:class="{
|
||||
'text-red': item.type === 'error',
|
||||
'text-orange': item.type === 'warning',
|
||||
'text-green': item.type === 'download',
|
||||
'text-contrast': item.type === 'success',
|
||||
'text-blue': !item.type || !['error', 'warning', 'success'].includes(item.type),
|
||||
'text-blue':
|
||||
!item.type ||
|
||||
!['error', 'warning', 'success', 'download'].includes(item.type),
|
||||
}"
|
||||
>
|
||||
<IssuesIcon v-if="item.type === 'warning'" class="h-5 w-5" />
|
||||
<DownloadIcon v-else-if="item.type === 'download'" class="h-5 w-5" />
|
||||
<CheckCircleIcon v-else-if="item.type === 'success'" class="h-5 w-5" />
|
||||
<XCircleIcon v-else-if="item.type === 'error'" class="h-5 w-5" />
|
||||
<InfoIcon v-else class="h-5 w-5" />
|
||||
@@ -47,6 +51,37 @@
|
||||
{{ item.text }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.progressItems?.length" class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="progressItem in item.progressItems"
|
||||
:key="progressItem.id"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<div class="text-contrast truncate">
|
||||
{{ progressItem.title }}
|
||||
</div>
|
||||
<ProgressBar
|
||||
:progress="progressItem.progress"
|
||||
:max="1"
|
||||
:waiting="progressItem.waiting"
|
||||
:color="progressColorForType(item.type)"
|
||||
:gradient-border="false"
|
||||
full-width
|
||||
/>
|
||||
<div v-if="progressItem.text" class="text-sm text-secondary truncate">
|
||||
{{ progressItem.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
v-else-if="item.progress != null || item.waiting"
|
||||
:progress="item.progress ?? 0"
|
||||
:max="1"
|
||||
:waiting="item.waiting ?? false"
|
||||
:color="progressColorForType(item.type)"
|
||||
:gradient-border="false"
|
||||
full-width
|
||||
/>
|
||||
<div v-if="item.buttons?.length" class="flex gap-1.5">
|
||||
<ButtonStyled
|
||||
v-for="(btn, idx) in item.buttons"
|
||||
@@ -65,7 +100,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, InfoIcon, IssuesIcon, XCircleIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
DownloadIcon,
|
||||
InfoIcon,
|
||||
IssuesIcon,
|
||||
XCircleIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import {
|
||||
@@ -74,6 +116,7 @@ import {
|
||||
type PopupNotificationButton,
|
||||
} from '../../providers'
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
import ProgressBar from '../base/ProgressBar.vue'
|
||||
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
const notifications = computed<PopupNotification[]>(() =>
|
||||
@@ -90,6 +133,21 @@ function handleButtonClick(id: string | number, btn: PopupNotificationButton) {
|
||||
popupNotificationManager.removeNotification(id)
|
||||
}
|
||||
|
||||
function progressColorForType(type: PopupNotification['type']) {
|
||||
if (type === 'error') {
|
||||
return 'red'
|
||||
} else if (type === 'warning') {
|
||||
return 'orange'
|
||||
} else if (type === 'download') {
|
||||
return 'green'
|
||||
} else if (type === 'success') {
|
||||
return 'green'
|
||||
} else if (type === 'info') {
|
||||
return 'blue'
|
||||
}
|
||||
return 'green'
|
||||
}
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
hasSidebar?: boolean
|
||||
|
||||
@@ -720,10 +720,10 @@
|
||||
"defaultMessage": "Import instance"
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.modpack-base.description": {
|
||||
"defaultMessage": "Use a popular modpack or upload one as your starting point."
|
||||
"defaultMessage": "Browse modpacks on Modrinth or import one from a file."
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.modpack-base.title": {
|
||||
"defaultMessage": "Modpack base"
|
||||
"defaultMessage": "Install modpack"
|
||||
},
|
||||
"creation-flow.modal.setup-type.option.vanilla-minecraft.description": {
|
||||
"defaultMessage": "Classic Minecraft with no mods or plugins."
|
||||
|
||||
@@ -6,11 +6,22 @@ export interface PopupNotificationButton {
|
||||
color?: 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'standard'
|
||||
}
|
||||
|
||||
export interface PopupNotificationProgressItem {
|
||||
id: string
|
||||
title: string
|
||||
text?: string
|
||||
progress: number
|
||||
waiting: boolean
|
||||
}
|
||||
|
||||
export interface PopupNotification {
|
||||
id: string | number
|
||||
title: string
|
||||
text?: string
|
||||
type?: 'error' | 'warning' | 'success' | 'info'
|
||||
type?: 'error' | 'warning' | 'success' | 'info' | 'download'
|
||||
progress?: number
|
||||
waiting?: boolean
|
||||
progressItems?: PopupNotificationProgressItem[]
|
||||
buttons?: PopupNotificationButton[]
|
||||
autoCloseMs?: number | null
|
||||
timer?: NodeJS.Timeout
|
||||
|
||||
@@ -107,6 +107,57 @@ export const Default: StoryObj = {
|
||||
})
|
||||
}
|
||||
|
||||
const showWaitingProgress = () => {
|
||||
popupManager.addPopupNotification({
|
||||
title: 'Installing modpack...',
|
||||
text: 'example-pack-1.0.0.mrpack',
|
||||
type: 'info',
|
||||
autoCloseMs: null,
|
||||
waiting: true,
|
||||
})
|
||||
}
|
||||
|
||||
const showDeterminateProgress = () => {
|
||||
popupManager.addPopupNotification({
|
||||
title: 'Downloading update',
|
||||
text: 'Downloading files...',
|
||||
type: 'success',
|
||||
autoCloseMs: null,
|
||||
progress: 0.62,
|
||||
})
|
||||
}
|
||||
|
||||
const showGroupedDownloads = () => {
|
||||
popupManager.addPopupNotification({
|
||||
title: 'Downloads',
|
||||
type: 'download',
|
||||
autoCloseMs: null,
|
||||
progressItems: [
|
||||
{
|
||||
id: 'java-21',
|
||||
title: 'Downloading Java 21',
|
||||
text: '42% Downloading runtime',
|
||||
progress: 0.42,
|
||||
waiting: false,
|
||||
},
|
||||
{
|
||||
id: 'pack-nebula',
|
||||
title: 'Nebula Pack',
|
||||
text: '8% Resolving files',
|
||||
progress: 0.08,
|
||||
waiting: false,
|
||||
},
|
||||
{
|
||||
id: 'assets',
|
||||
title: 'Assets',
|
||||
text: 'Preparing...',
|
||||
progress: 0,
|
||||
waiting: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
popupManager.clearAllNotifications()
|
||||
}
|
||||
@@ -118,6 +169,9 @@ export const Default: StoryObj = {
|
||||
showInfo,
|
||||
showNoButtons,
|
||||
showPermanent,
|
||||
showWaitingProgress,
|
||||
showDeterminateProgress,
|
||||
showGroupedDownloads,
|
||||
clearAll,
|
||||
}
|
||||
},
|
||||
@@ -142,6 +196,15 @@ export const Default: StoryObj = {
|
||||
<ButtonStyled>
|
||||
<button @click="showPermanent">Permanent</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="showWaitingProgress">Waiting Progress</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="showDeterminateProgress">Determinate Progress</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="showGroupedDownloads">Grouped Downloads</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="clearAll">Clear All</button>
|
||||
</ButtonStyled>
|
||||
|
||||
Reference in New Issue
Block a user