Skip to content

Commit 16766be

Browse files
authored
Add server unzipping (#3622)
* Initial unzipping feature * Remove explicit backup provider naming from frontend * CF placeholder * Use regex for CF links * Lint * Add unzip warning for conflicting files, fix hydration error * Adjust conflict modal ui * Fix old queued ops sticking around, remove conflict warning * Add vscode "editor.detectIndentation": true
1 parent 1884410 commit 16766be

23 files changed

+1037
-250
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"prettier.endOfLine": "lf",
33
"editor.formatOnSave": true,
44
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
5+
"editor.detectIndentation": true,
56
"editor.codeActionsOnSave": {
67
"source.fixAll.eslint": "explicit"
78
}

apps/frontend/.env.prod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
BASE_URL=https://api.modrinth.com/v2/
2+
BROWSER_BASE_URL=https://api.modrinth.com/v2/
3+
PYRO_BASE_URL=https://archon.modrinth.com
4+
PROD_OVERRIDE=true

apps/frontend/.env.staging

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
BASE_URL=https://staging-api.modrinth.com/v2/
2+
BROWSER_BASE_URL=https://staging-api.modrinth.com/v2/
3+
PYRO_BASE_URL=https://staging-archon.modrinth.com
4+
PROD_OVERRIDE=true
Lines changed: 112 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,140 @@
11
<template>
2-
<div class="vue-notification-group">
2+
<div class="vue-notification-group experimental-styles-within">
33
<transition-group name="notifs">
44
<div
55
v-for="(item, index) in notifications"
66
:key="item.id"
77
class="vue-notification-wrapper"
8-
@click="notifications.splice(index, 1)"
98
@mouseenter="stopTimer(item)"
109
@mouseleave="setNotificationTimer(item)"
1110
>
12-
<div class="vue-notification-template vue-notification" :class="{ [item.type]: true }">
13-
<div class="notification-title" v-html="item.title"></div>
14-
<div class="notification-content" v-html="item.text"></div>
11+
<div class="flex w-full gap-2 overflow-hidden rounded-lg bg-bg-raised shadow-xl">
12+
<div
13+
class="w-2"
14+
:class="{
15+
'bg-red': item.type === 'error',
16+
'bg-orange': item.type === 'warning',
17+
'bg-green': item.type === 'success',
18+
'bg-blue': !item.type || !['error', 'warning', 'success'].includes(item.type),
19+
}"
20+
></div>
21+
<div
22+
class="grid w-full grid-cols-[auto_1fr_auto] items-center gap-x-2 gap-y-1 py-2 pl-1 pr-3"
23+
>
24+
<div
25+
class="flex items-center"
26+
:class="{
27+
'text-red': item.type === 'error',
28+
'text-orange': item.type === 'warning',
29+
'text-green': item.type === 'success',
30+
'text-blue': !item.type || !['error', 'warning', 'success'].includes(item.type),
31+
}"
32+
>
33+
<IssuesIcon v-if="item.type === 'warning'" class="h-6 w-6" />
34+
<CheckCircleIcon v-else-if="item.type === 'success'" class="h-6 w-6" />
35+
<XCircleIcon v-else-if="item.type === 'error'" class="h-6 w-6" />
36+
<InfoIcon v-else class="h-6 w-6" />
37+
</div>
38+
<div class="m-0 text-wrap font-bold text-contrast" v-html="item.title"></div>
39+
<div class="flex items-center gap-1">
40+
<div v-if="item.count && item.count > 1" class="text-xs font-bold text-contrast">
41+
x{{ item.count }}
42+
</div>
43+
<ButtonStyled circular size="small">
44+
<button v-tooltip="'Copy to clipboard'" @click="copyToClipboard(item)">
45+
<CheckIcon v-if="copied[createNotifText(item)]" />
46+
<CopyIcon v-else />
47+
</button>
48+
</ButtonStyled>
49+
<ButtonStyled circular size="small">
50+
<button v-tooltip="`Dismiss`" @click="notifications.splice(index, 1)">
51+
<XIcon />
52+
</button>
53+
</ButtonStyled>
54+
</div>
55+
<div></div>
56+
<div class="col-span-2 text-sm text-primary" v-html="item.text"></div>
57+
<template v-if="item.errorCode">
58+
<div></div>
59+
<div
60+
class="m-0 text-wrap text-xs font-medium text-secondary"
61+
v-html="item.errorCode"
62+
></div>
63+
</template>
64+
</div>
1565
</div>
1666
</div>
1767
</transition-group>
1868
</div>
1969
</template>
2070
<script setup>
71+
import { ButtonStyled } from "@modrinth/ui";
72+
import {
73+
XCircleIcon,
74+
CheckCircleIcon,
75+
CheckIcon,
76+
InfoIcon,
77+
IssuesIcon,
78+
XIcon,
79+
CopyIcon,
80+
} from "@modrinth/assets";
2181
const notifications = useNotifications();
2282
2383
function stopTimer(notif) {
2484
clearTimeout(notif.timer);
2585
}
26-
</script>
27-
<style lang="scss" scoped>
28-
.vue-notification {
29-
background: var(--color-blue) !important;
30-
border-left: 5px solid var(--color-blue) !important;
31-
color: var(--color-brand-inverted) !important;
3286
33-
box-sizing: border-box;
34-
text-align: left;
35-
font-size: 12px;
36-
padding: 10px;
37-
margin: 0 5px 5px;
87+
const copied = ref({});
3888
39-
&.success {
40-
background: var(--color-green) !important;
41-
border-left-color: var(--color-green) !important;
89+
const createNotifText = (notif) => {
90+
let text = "";
91+
if (notif.title) {
92+
text += notif.title;
4293
}
43-
44-
&.warn {
45-
background: var(--color-orange) !important;
46-
border-left-color: var(--color-orange) !important;
94+
if (notif.text) {
95+
if (text.length > 0) {
96+
text += "\n";
97+
}
98+
text += notif.text;
4799
}
48-
49-
&.error {
50-
background: var(--color-red) !important;
51-
border-left-color: var(--color-red) !important;
100+
if (notif.errorCode) {
101+
if (text.length > 0) {
102+
text += "\n";
103+
}
104+
text += notif.errorCode;
52105
}
53-
}
106+
return text;
107+
};
108+
109+
function copyToClipboard(notif) {
110+
const text = createNotifText(notif);
54111
112+
copied.value[text] = true;
113+
navigator.clipboard.writeText(text);
114+
setTimeout(() => {
115+
delete copied.value[text];
116+
}, 2000);
117+
}
118+
</script>
119+
<style lang="scss" scoped>
55120
.vue-notification-group {
56121
position: fixed;
57-
right: 25px;
58-
bottom: 25px;
59-
z-index: 99999999;
60-
width: 300px;
122+
right: 1.5rem;
123+
bottom: 1.5rem;
124+
z-index: 200;
125+
width: 450px;
126+
127+
@media screen and (max-width: 500px) {
128+
width: calc(100% - 0.75rem * 2);
129+
right: 0.75rem;
130+
bottom: 0.75rem;
131+
}
61132
62133
.vue-notification-wrapper {
63134
width: 100%;
64135
overflow: hidden;
65136
margin-bottom: 10px;
66137
67-
.vue-notification-template {
68-
border-radius: var(--size-rounded-card);
69-
margin: 0;
70-
71-
.notification-title {
72-
font-size: var(--font-size-lg);
73-
margin-right: auto;
74-
font-weight: 600;
75-
}
76-
77-
.notification-content {
78-
margin-right: auto;
79-
font-size: var(--font-size-md);
80-
}
81-
}
82-
83138
&:last-child {
84139
margin: 0;
85140
}
@@ -98,10 +153,18 @@ function stopTimer(notif) {
98153
.notifs-enter-active,
99154
.notifs-leave-active,
100155
.notifs-move {
101-
transition: all 0.5s;
156+
transition: all 0.25s ease-in-out;
102157
}
103158
.notifs-enter-from,
104159
.notifs-leave-to {
105160
opacity: 0;
106161
}
162+
163+
.notifs-enter-from {
164+
transform: translateY(100%) scale(0.8);
165+
}
166+
167+
.notifs-leave-to {
168+
transform: translateX(100%) scale(0.8);
169+
}
107170
</style>

apps/frontend/src/components/ui/servers/FileItem.vue

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<ButtonStyled circular type="transparent">
5454
<UiServersTeleportOverflowMenu :options="menuOptions" direction="left" position="bottom">
5555
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
56+
<template #extract><PackageOpenIcon /> Extract</template>
5657
<template #rename><EditIcon /> Rename</template>
5758
<template #move><RightArrowIcon /> Move</template>
5859
<template #download><DownloadIcon /> Download</template>
@@ -73,6 +74,8 @@ import {
7374
FolderOpenIcon,
7475
FileIcon,
7576
RightArrowIcon,
77+
PackageOpenIcon,
78+
FileArchiveIcon,
7679
} from "@modrinth/assets";
7780
import { computed, shallowRef, ref } from "vue";
7881
import { renderToString } from "vue/server-renderer";
@@ -99,15 +102,14 @@ interface FileItemProps {
99102
const props = defineProps<FileItemProps>();
100103
101104
const emit = defineEmits<{
102-
(e: "rename", item: { name: string; type: string; path: string }): void;
103-
(e: "move", item: { name: string; type: string; path: string }): void;
105+
(
106+
e: "rename" | "move" | "download" | "delete" | "edit" | "extract",
107+
item: { name: string; type: string; path: string },
108+
): void;
104109
(
105110
e: "moveDirectTo",
106111
item: { name: string; type: string; path: string; destination: string },
107112
): void;
108-
(e: "download", item: { name: string; type: string; path: string }): void;
109-
(e: "delete", item: { name: string; type: string; path: string }): void;
110-
(e: "edit", item: { name: string; type: string; path: string }): void;
111113
(e: "contextmenu", x: number, y: number): void;
112114
}>();
113115
@@ -143,6 +145,7 @@ const codeExtensions = Object.freeze([
143145
144146
const textExtensions = Object.freeze(["txt", "md", "log", "cfg", "conf", "properties", "ini"]);
145147
const imageExtensions = Object.freeze(["png", "jpg", "jpeg", "gif", "svg", "webp"]);
148+
const supportedArchiveExtensions = Object.freeze(["zip"]);
146149
const units = Object.freeze(["B", "KB", "MB", "GB", "TB", "PB", "EB"]);
147150
148151
const route = shallowRef(useRoute());
@@ -156,7 +159,18 @@ const containerClasses = computed(() => [
156159
157160
const fileExtension = computed(() => props.name.split(".").pop()?.toLowerCase() || "");
158161
162+
const isZip = computed(() => fileExtension.value === "zip");
163+
159164
const menuOptions = computed(() => [
165+
{
166+
id: "extract",
167+
shown: isZip.value,
168+
action: () => emit("extract", { name: props.name, type: props.type, path: props.path }),
169+
},
170+
{
171+
divider: true,
172+
shown: isZip.value,
173+
},
160174
{
161175
id: "rename",
162176
action: () => emit("rename", { name: props.name, type: props.type, path: props.path }),
@@ -189,6 +203,7 @@ const iconComponent = computed(() => {
189203
if (codeExtensions.includes(ext)) return UiServersIconsCodeFileIcon;
190204
if (textExtensions.includes(ext)) return UiServersIconsTextFileIcon;
191205
if (imageExtensions.includes(ext)) return UiServersIconsImageFileIcon;
206+
if (supportedArchiveExtensions.includes(ext)) return FileArchiveIcon;
192207
return FileIcon;
193208
});
194209

apps/frontend/src/components/ui/servers/FileVirtualList.vue

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
:size="item.size"
3131
@delete="$emit('delete', item)"
3232
@rename="$emit('rename', item)"
33+
@extract="$emit('extract', item)"
3334
@download="$emit('download', item)"
3435
@move="$emit('move', item)"
3536
@move-direct-to="$emit('moveDirectTo', $event)"
@@ -49,14 +50,12 @@ const props = defineProps<{
4950
}>();
5051
5152
const emit = defineEmits<{
52-
(e: "delete", item: any): void;
53-
(e: "rename", item: any): void;
54-
(e: "download", item: any): void;
55-
(e: "move", item: any): void;
56-
(e: "edit", item: any): void;
53+
(
54+
e: "delete" | "rename" | "download" | "move" | "edit" | "moveDirectTo" | "extract",
55+
item: any,
56+
): void;
5757
(e: "contextmenu", item: any, x: number, y: number): void;
5858
(e: "loadMore"): void;
59-
(e: "moveDirectTo", item: any): void;
6059
}>();
6160
6261
const ITEM_HEIGHT = 61;

apps/frontend/src/components/ui/servers/FilesBrowseNavbar.vue

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,29 +117,46 @@
117117
</div>
118118

119119
<ButtonStyled type="transparent">
120-
<UiServersTeleportOverflowMenu
120+
<OverflowMenu
121+
:dropdown-id="`create-new-${baseId}`"
121122
position="bottom"
122123
direction="left"
123124
aria-label="Create new..."
124125
:options="[
125126
{ id: 'file', action: () => $emit('create', 'file') },
126127
{ id: 'directory', action: () => $emit('create', 'directory') },
127128
{ id: 'upload', action: () => $emit('upload') },
129+
{ divider: true },
130+
{ id: 'upload-zip', shown: false, action: () => $emit('upload-zip') },
131+
{ id: 'install-from-url', action: () => $emit('unzip-from-url', false) },
132+
{ id: 'install-cf-pack', action: () => $emit('unzip-from-url', true) },
128133
]"
129134
>
130135
<PlusIcon aria-hidden="true" />
131136
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" />
132137
<template #file> <BoxIcon aria-hidden="true" /> New file </template>
133138
<template #directory> <FolderOpenIcon aria-hidden="true" /> New folder </template>
134139
<template #upload> <UploadIcon aria-hidden="true" /> Upload file </template>
135-
</UiServersTeleportOverflowMenu>
140+
<template #upload-zip>
141+
<FileArchiveIcon aria-hidden="true" /> Upload from .zip file
142+
</template>
143+
<template #install-from-url>
144+
<LinkIcon aria-hidden="true" /> Upload from .zip URL
145+
</template>
146+
<template #install-cf-pack>
147+
<CurseForgeIcon aria-hidden="true" /> Install CurseForge pack
148+
</template>
149+
</OverflowMenu>
136150
</ButtonStyled>
137151
</div>
138152
</header>
139153
</template>
140154

141155
<script setup lang="ts">
142156
import {
157+
LinkIcon,
158+
CurseForgeIcon,
159+
FileArchiveIcon,
143160
BoxIcon,
144161
PlusIcon,
145162
UploadIcon,
@@ -150,20 +167,22 @@ import {
150167
ChevronRightIcon,
151168
FilterIcon,
152169
} from "@modrinth/assets";
153-
import { ButtonStyled } from "@modrinth/ui";
170+
import { ButtonStyled, OverflowMenu } from "@modrinth/ui";
154171
import { ref, computed } from "vue";
155172
import { useIntersectionObserver } from "@vueuse/core";
156173
157174
const props = defineProps<{
158175
breadcrumbSegments: string[];
159176
searchQuery: string;
160177
currentFilter: string;
178+
baseId: string;
161179
}>();
162180
163181
defineEmits<{
164182
(e: "navigate", index: number): void;
165183
(e: "create", type: "file" | "directory"): void;
166-
(e: "upload"): void;
184+
(e: "upload" | "upload-zip"): void;
185+
(e: "unzip-from-url", cf: boolean): void;
167186
(e: "update:searchQuery", value: string): void;
168187
(e: "filter", type: string): void;
169188
}>();

0 commit comments

Comments
 (0)