Compare commits

...

170 Commits

Author SHA1 Message Date
Alejandro González
61c2ce2107 Fix lint checks 2025-04-26 23:27:15 +02:00
Alejandro González
d37c634216 Merge branch 'main' into app-users-orgs-and-more 2025-04-26 23:22:34 +02:00
Prospector
25016053ca Update changelog 2025-04-25 19:41:41 -07:00
Emma Alexia
f9c0c1bc53 Clarify that Modrinth Servers prices are in USD (#3553)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-04-26 02:35:30 +00:00
Prospector
73e54a5fbb Add Servers cancellation survey (#3551) 2025-04-25 19:31:36 +00:00
Erb3
6f902e2107 feat(labrinth): environment variables for more customizable SMTP (#2886)
* refactor: move .env to .env.example

* refactor(labrinth): allow setting SMTP port and TLS

This will help setting up labrinth for local development. You can now use a mock SMTP server such as smtp4dev. The TLS options will stay the same as before if set to `true`, and disabled when `false`.

Depends on #2883

* chore(labrinth): lint

* chore(labrinth): conflicts

* chore(labrinth): conflicts

* fix: use TLS port by default

Co-authored-by: AlexTMjugador<me@alegon.dev>

Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>

* fix(labrinth): correct deafult SMTP port in .env

* feat(labrinth): expose all SMTP TLS settings

Replaced if/else with a switch statement. The new values for `SMPT_TLS` are `none`, `opportunistic_start_tls`, `requires_start_tls`, `tls`. When none of these values are supplied, it defaults to full TLS (`tls`), and throws a warning.

Resolves PR review

* fix(labrinth): correct SMTP TLS example .env setting

Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>

* fix(labrinth) SMTP tls env var check

Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>

---------

Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>
Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
2025-04-22 11:29:14 +00:00
Alejandro González
ecb1379585 enh(labrinth): proper Clickhouse user setup for local development (#3545) 2025-04-21 15:44:24 +00:00
Alejandro González
068711e7a9 enh(labrinth): disable hCaptcha verification when secret is unset (#3544) 2025-04-21 15:42:17 +00:00
maksimetny
f695fe0ee7 Adds drag region support for touch screens (#3178)
https://github.com/tauri-apps/tauri/issues/4746#issuecomment-2007114269

Signed-off-by: maksimetny <46288028+maksimetny@users.noreply.github.com>
2025-04-19 19:18:37 +00:00
Aaron Müller
6cdc07406d Use the new ConfirmModal (#3458)
* style: copy from old modal

* chore: move to new modals

* eslint: fix sorting

---------

Signed-off-by: Aaron Müller <160637865+amueller0@users.noreply.github.com>
2025-04-19 14:50:30 +00:00
Erb3
daf6999111 Document usage of OAuth (#3342)
* docs: hitchhiker's guide to OAuth

* docs: remove old OAuth guide from OpenApi spec

* fixup! docs: remove old OAuth guide from OpenApi spec

* docs: mention /user endpoint in oauth

* docs: oauth flow overview

* docs: mention PAT

* docs: fix reviews

* docs: support portal over github issue

Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>

---------

Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>
2025-04-19 13:51:01 +00:00
Magnus Jensen
42731521f1 fix: instance project author link not checking for organizations (#3315)
Co-authored-by: Emma Alexia <emma@modrinth.com>
2025-04-19 13:44:46 +00:00
Calum H.
182119aedf feat: modrinth maven developer mode additions (#3498)
* feat: modrinth maven copy string in developer mode

* feat: Modrinth maven in versions list dropdown for developer mode.

* fix: lint
2025-04-19 13:10:34 +00:00
MikeyPants
59e18b3104 fix(apps/frontend): error data can be undefined (#3513) 2025-04-19 13:05:43 +00:00
Emma Alexia
5c1f198397 Add ability to delete user icon (#3383)
* Add user icon delete route

By request of moderation, but also just generally nice to have

* Add relevant docs and frontend

* Add v2 version
2025-04-19 12:49:23 +00:00
Emma Alexia
3cd6718384 Fix inverted condition with refunds (#3539)
Signed-off-by: Emma Alexia <emma@modrinth.com>
2025-04-19 12:25:43 +00:00
Prospector
1903980b71 Update Servers marketing page (#3535)
* Update Servers marketing page

* Add burst FAQ

* Updated phrasing again

* Fix servers page when not logged in

* Update changelog
2025-04-18 22:23:30 -07:00
Emma Alexia
84a28e045b Allow servers to be unprovisioned without issuing a refund (#3534)
* Allow servers to be unprovisioned without issuing a refund

for very specific weird circumstances where a server gets stuck/etc; useful for support

* still create a charge

* Fix compile
2025-04-19 04:39:18 +00:00
Emma Alexia
d0aef27f7b Fix deleting a user that has a prior subscription (#3533)
(for real this time)
2025-04-19 04:38:54 +00:00
Prospector
d6a74b0cfe Fix prepare initiating overriding states, and add handling of failed file prep 2025-04-17 02:56:26 -07:00
Prospector
0c43eb0d22 Update changelog date 2025-04-17 02:19:06 -07:00
Sticks
f8494030aa backup page fixes and new impls for new apis (#3437)
* wip: backup page fixes and new impls for new apis

* wip: more progress on backup fixes, almost done

* lint

* Backups cleanup

* Don't show create warning if creating

* Fix ongoing state

* Download support

* Support ready

* Disable auto backup button

* Use auth param for download of backups

* Disable install buttons when backup is in progress, add retrying

* Make prepare button have immediate feedback, don't refresh backups in all cases

* Intl:extract & rebase fixes

* Updated changelog and fix lint

---------

Co-authored-by: Prospector <prospectordev@gmail.com>
2025-04-17 01:26:13 -07:00
Prospector
817151e47c Add notice dismissed count to dashboard 2025-04-15 18:11:32 -07:00
Prospector
d5dfb609cf Update changelog 2025-04-15 16:31:52 -07:00
Prospector
09aae0edc9 Update changelog 2025-04-15 16:31:52 -07:00
Prospector
6aa6db4e8c Survey notices for Servers (#3514)
* Survey notices for Servers

* lint

* remove creepy frog
2025-04-15 16:29:50 -07:00
Jai Agrawal
76be502e16 Make connections short-lived redis (#3509) 2025-04-15 15:39:13 -07:00
Prospector
04659a8198 Notices fixes and support for titles (#3508)
* Notices fixes and support for titles

* Lint
2025-04-14 16:58:31 -07:00
Prospector
6c16688ca9 Remove notice limit 2025-04-12 22:23:45 -07:00
Emma Alexia
f2ec89e62b Fix two database errors (#3483)
* Fixes error when an admin tries transferring project ownership
* Fixes error when trying to delete a user when they previously have a transaction

Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-04-12 22:21:02 -07:00
Prospector
edd09b0b16 Update changelog 2025-04-12 22:06:22 -07:00
Prospector
59edc8d618 Add notices system to Servers (#3502)
* Servers notices

* Refresh on unassign
2025-04-12 22:00:22 -07:00
Aaron Müller
56520572b2 fix: dropdown import (#3459)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Emma Alexia <emma@modrinth.com>
2025-04-12 16:22:16 +00:00
Alejandro González
487bdd1e48 chore: remove unused Rust dependencies (#3492) 2025-04-12 15:42:45 +00:00
Tiziano
8ad5e011ca Update 1-app-bug.yml (#3486)
Signed-off-by: Tiziano <69322987+T1xx1@users.noreply.github.com>
2025-04-12 15:22:41 +00:00
un_pogaz
6f43fc272b fix: use local assets for "apps/labrinth/README.md" (#3489) 2025-04-12 15:21:20 +00:00
Alejandro González
e008b657a5 Fix Clippy lints (#3494)
* chore: fix some Clippy lints

* chore(labrinth): more Clippy fixes
2025-04-12 13:45:17 +00:00
Emma Alexia
365367dd16 Hide collections with no projects from public view (#3408)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-04-06 20:54:18 +00:00
Jai A
36367e475e fix margin 2025-04-04 00:49:58 -07:00
Jai A
13f2961e43 run migrate 2025-04-03 17:47:30 -07:00
Jai A
69b70d70a8 Remove lock, fix billing job 2025-04-03 16:14:27 -07:00
Jai A
d0d0dcf09f Fix billing setup 2025-04-03 15:44:53 -07:00
Prospector
41b9729b9b Update changelog 2025-04-01 21:03:40 -07:00
Prospector
a2009cae39 Revert "fixed a bug"
This reverts commit 49faba6ad2.
2025-04-01 20:54:05 -07:00
Prospector
fab086b3e1 Revert "favicon"
This reverts commit e8f8be1940.
2025-04-01 20:54:05 -07:00
Prospector
f379126242 Revert "more improvements (#3449)"
This reverts commit 3d2cef40d5.
2025-04-01 20:54:04 -07:00
Prospector
8e0d9f2da6 Revert "Add craftmine support"
This reverts commit 4624a29332.
2025-04-01 20:54:04 -07:00
Prospector
e931b5c8ef Revert "Pizza highlight"
This reverts commit 9024b2eec5.
2025-04-01 20:54:04 -07:00
Prospector
84617d0c49 Revert "bypass discord cache"
This reverts commit 9b442d04d9.
2025-04-01 20:54:04 -07:00
Prospector
0908cf4e94 Revert "use modrinth links"
This reverts commit 916f27c5ab.
2025-04-01 20:54:04 -07:00
Prospector
916f27c5ab use modrinth links 2025-04-01 10:32:52 -07:00
Prospector
9b442d04d9 bypass discord cache 2025-04-01 09:55:29 -07:00
Prospector
9024b2eec5 Pizza highlight 2025-04-01 09:51:04 -07:00
Prospector
4624a29332 Add craftmine support 2025-04-01 09:50:00 -07:00
Sticks
3d2cef40d5 more improvements (#3449)
* more improvements

* fix: apply pnpm run fix
2025-03-31 20:34:49 -07:00
Prospector
e8f8be1940 favicon 2025-03-31 17:53:57 -07:00
Prospector
49faba6ad2 fixed a bug 2025-03-31 17:21:31 -07:00
Jai A
b9d90aa635 Rebuild daedalus 2025-03-26 21:51:46 -07:00
Nitrrine
5bcf65dd67 Limit project version number to 32 chars (#3425) 2025-03-26 03:43:58 +00:00
JakobDev
742d2ed9c3 Fix MimeType on Linux Desktop File (#3313)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-03-26 01:18:02 +00:00
Prospector
86128f953a Update changelog 2025-03-25 18:17:53 -07:00
moehreag
b8e5a6944e fix: hide resubmit nag for draft projects (#3354)
Co-authored-by: Emma Alexia <emma@modrinth.com>
2025-03-25 18:17:35 -07:00
Prospector
4508fad588 Attempt to fix displayName undefined error (#3424) 2025-03-26 01:04:50 +00:00
Jan Straßburger
fd2f500038 Fix correct 'versions' description in Statistics schema (#3396)
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Emma Alexia <emma@modrinth.com>
2025-03-25 18:20:36 +00:00
Prospector
a20374d6e3 Fix errors with intl extraction, extract error.vue (#3421)
* Fix errors with intl extraction, extract error.vue

* Update changelog and fix lint
2025-03-25 10:38:04 -07:00
Prospector
ffc69dbaba Update changelog 2025-03-25 09:22:51 -07:00
Prospector
6b98655069 Fix modrinth logo on home page 2025-03-25 09:19:43 -07:00
Jai Agrawal
b5a9a93323 Distributed rate limit, fix search panic, add migration task (#3419)
* Distributed rate limit, fix search panic, add migration task

* Add binary info to root endpoint
2025-03-25 01:10:43 -07:00
Jai A
5fbf5b22c0 Update ads.txt 2025-03-22 10:24:24 -07:00
Josiah Glosson
99cd96faa8 Use the log config from the Vanilla client.json (#3411)
* Use the log config from the Vanilla client.json

* Remove debug message

---------

Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-03-20 13:42:37 -07:00
Erb3
c4b60f1720 Prefer icons from modrinth/assets (#3394)
Replaced all icon usages of `apps/frontend/src/assets/image/utils` for `@modrinth/assets`.

The only icon which has been changed is the `WorldIcon`, which has been replaced by the `GlobeIcon`.
2025-03-18 18:28:23 -07:00
Prospector
a19bf3dc0e Servers: Only apply game version filters when searching for non-modpack projects, auto-populate plugin loaders (#3403) 2025-03-18 18:27:15 -07:00
Prospector
77021d2af8 Handle downtime errors, give more information on error pages. (#3402)
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-03-18 18:26:57 -07:00
Prospector
16893ec0e3 Fix display of critical announcements in app (#3407) 2025-03-18 18:24:14 -07:00
Jai A
d49cc87b8c only run migrations on prod instances 2025-03-15 08:32:45 -07:00
Josiah Glosson
c998d2566e Allow multiple labrinth instances (#3360)
* Move a lot of scheduled tasks to be runnable from the command-line

* Use pubsub to handle sockets connected to multiple Labrinths

* Clippy fix

* Fix build and merge some stuff

* Fix build fmt
:

---------

Signed-off-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-03-15 07:28:20 -07:00
Prospector
84a9438a70 Update changelog 2025-03-13 19:24:34 -07:00
Prospector
09ae3515f7 Update changelog 2025-03-13 19:22:59 -07:00
Prospector
b665c17be8 Update servers marketing page (#3399)
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-03-13 19:19:40 -07:00
Josiah Glosson
eccd852426 Add log config parsing support to daedelus (#3395) 2025-03-13 13:04:44 -07:00
Tiziano
827e3ec0a0 fix(frontend): mobile navbar covers legal disclaimer in the bottom of the footer (#3366) 2025-03-12 09:53:55 -07:00
Erb3
801c03981a refactor(web): properly create table in cmp-info according to spec (#3362)
Stops the vue compiler from nagging you, and improves consistency with other tables.
2025-03-12 09:53:44 -07:00
Emma Alexia
31a001bbc1 Fix moderation review page (#3389) 2025-03-12 09:53:11 -07:00
Prospector
621ed5fb02 Fix wording on CMP info page (#3385) 2025-03-11 23:28:17 -07:00
Jai Agrawal
887e437d35 Move archon to env var (#3386) 2025-03-11 23:27:49 -07:00
Emma Alexia
1ea196051f Update CCPA notice with updated information for Servers (#3384) 2025-03-11 17:00:30 -07:00
felix
366f528853 Fix but better (#3376)
Signed-off-by: felix <60808107+ItsFelix5@users.noreply.github.com>
2025-03-11 19:46:31 +00:00
Josiah Glosson
d6c8af7ed5 Fix Labrinth not compiling on Windows due to jemalloc dependency (#3378) 2025-03-10 21:45:36 +00:00
Jai Agrawal
4dd33a2f9e Fix issues being excluded by delphi (#3374) 2025-03-09 17:15:07 -07:00
Jai Agrawal
1009695a15 Support jemalloc profiling and heap dumps (#3373) 2025-03-09 14:41:19 -07:00
Jai A
d3427375b0 fix lint 2025-03-09 13:21:15 -07:00
Jai Agrawal
5c8ed9a8ca Tracing support (#3372)
* Tracing support

* Add console subscriber

* Add console subscriber
2025-03-09 13:01:24 -07:00
Jai Agrawal
9c5d817a8a Add prom metrics for database, use redis max conn var (#3359) 2025-03-07 17:16:45 -08:00
Prospector
d51a1c47c7 Update changelog 2025-03-05 17:37:45 -08:00
Prospector
2b7378bd64 Fix moderation-side issues (#3345)
* Fix moderation-side issues by segmenting requests in review page and handling missing users in report page

* increase to 100

* 450 limit

* fine! take 1000!
2025-03-05 17:30:41 -08:00
Jai Agrawal
c1bb934fc6 Add clickhouse replication, exclude bad prom metrics (#3344) 2025-03-05 15:40:46 -08:00
Michael
0d223e3ab5 Use dumb-init as entrypoint (#3343) 2025-03-05 15:40:27 -08:00
Prospector
b704e0c8ed Update changelog 2025-03-05 12:36:57 -08:00
Prospector
79279479b1 Minor bugfixes (#3338)
* Workaround linux firefox repeat issue

* Nullcheck onShow/onHide functions
2025-03-05 12:28:48 -08:00
Prospector
ee4d7c88f1 Fix lint 2025-03-04 10:06:11 -08:00
Prospector
09023f2b49 Update changelog 2025-03-03 22:23:53 -08:00
Prospector
36cfcc2093 Admin & staff page enhancements (#3333) 2025-03-03 22:22:25 -08:00
Prospector
c2d455f166 Add random project easter egg (#3335) 2025-03-03 22:22:10 -08:00
Jai Agrawal
6859509eb5 Fix all tests (#3332) 2025-03-03 16:05:39 -08:00
Prospector
cd2fcc06fe Merge branch 'main' into app-users-orgs-and-more 2025-03-03 12:48:04 -08:00
Tiziano
e2de39ad83 chore: update docs deps/pkgs (#3326)
* chore: run astro upgrade cli cmd

* chore: move content config & add content loader

* chore: update pkgs

* fix: bump starlight-openapi

* fix: bump sharp

* fix: update pnpm-lock
2025-03-03 11:15:48 -08:00
Prospector
ca2307e609 Fix changelog 2025-03-02 18:45:09 -08:00
Prospector
0c58b5b83d Update changelog 2025-03-02 18:40:55 -08:00
Prospector
74a12bd606 Add scrollbar to moderation checklist when too tall (#3319) 2025-03-02 18:37:31 -08:00
Prospector
9bb9e13ee8 Add copy ID button to versions list (#3327) 2025-03-02 18:37:21 -08:00
Jai Agrawal
19787a3f51 Subpackage common -> ariadne (#3323)
* Subpackage common -> ariadne

* add common

* Remove build

* only build labrinth

* common

* set sqlx offline

* copy dirs

* Fix build
2025-03-01 20:53:43 -08:00
Josiah Glosson
650ab71a83 Commonized networking (#3310)
* Fix not being able to connect to local friends socket

* Start basic work on tunneling protocol and move some code into a common crate

* Commonize message serialization logic

* Serialize Base62Ids as u64 when human-readability is not required

* Move ActiveSockets tuple into struct

* Make CI run when rust-common is updated

CI is currently broken for labrinth, however

* Fix theseus-release.yml to reference itself correctly

* Implement Labrinth side of tunneling

* Implement non-friend part of theseus tunneling

* Implement client-side except for socket loop

* Implement the socket loop

Doesn't work though. Debugging time!

* Fix config.rs

* Fix deadlock in labrinth socket handling

* Update dockerfile

* switch to workspace prepare at root level

* Wait for connection before tunneling in playground

* Move rust-common into labrinth

* Remove rust-common references from Actions

* Revert "Update dockerfile"

This reverts commit 3caad59bb4.

* Fix Docker build

* Rebuild Theseus if common code changes

* Allow multiple connections from the same user

* Fix test building

* Move FriendSocketListening and FriendSocketStoppedListening to non-panicking TODO for now

* Make message_serialization macro take varargs for binary messages

* Improve syntax of message_serialization macro

* Remove the ability to connect to a virtual socket, and disable the ability to listen on one

* Allow the app to compile without running labrinth

* Clippy fix

* Update Rust and Clippy fix again

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
2025-02-28 10:52:47 -08:00
Prospector
90def724c2 Update changelog 2025-02-25 10:11:26 -08:00
Prospector
f357275fd3 Fix upgrades being allowed when out of stock (#3307) 2025-02-25 10:07:59 -08:00
Erb3
2c2a13b587 fix(frontend): make collection summary optional in editing (#3304)
Forgot to fix it here too in #3292.
Reported by @falseresync
2025-02-25 17:53:29 +00:00
Prospector
b3a664e0d4 Improve clarity of ongoing revenue period notice (#3301)
* Improve clarity of ongoing revenue period notice

* get rid of semicolon

---------

Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-02-24 19:01:41 -08:00
Prospector
3140dab99d Move Minecraft brand disclaimer to bottom of footer (#3302)
* Move Minecraft brand disclaimer to bottom of footer

* Add careers link back to footer

* Intl extract

---------

Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-02-24 19:00:42 -08:00
Josiah Glosson
a74b2da147 Change app-lib io::rename to io::rename_or_move (#3251)
Also add io::is_same_disk and io::create_dir

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-02-24 19:00:17 -08:00
Prospector
a731f4758e Remove unused code 2025-02-21 14:18:56 -08:00
Prospector
0e0fce0e66 Extract intl, fix lint 2025-02-21 14:17:28 -08:00
Prospector
ea2f97ae23 Add flag for new projects list on user page, update background gradient colors 2025-02-21 14:17:28 -08:00
Prospector
2a0722d0d0 Remove old prop and fix error with card actions on pages with instance context 2025-02-21 14:17:28 -08:00
Prospector
adf213f32a Begin collections implementation 2025-02-21 14:17:28 -08:00
Prospector
54f408dc6c Fix user link in content page 2025-02-21 14:17:28 -08:00
Prospector
c82c4ddc5b Add users, orgs, and new project cards to App 2025-02-21 14:17:28 -08:00
Prospector
701fef08f8 Make debug info always expanded to aid support team, add copy button (#3282)
* Make debug info always expanded to aid support team, add copy button

* Remove testing error lol
2025-02-21 21:50:43 +00:00
Prospector
37ecf75087 Fix lint on toggle 2025-02-21 13:22:56 -08:00
Prospector
4c99e379f2 Update changelog for Feb 21st release 2025-02-21 13:20:18 -08:00
Erb3
fc2c740843 fix(frontend): make collection summary optional (#3292)
Reported by @falseresync
2025-02-21 21:09:26 +00:00
Erb3
1358336a76 refactor(ui): move Chips component to composition API + TS (#3288)
* refactor(ui): move Chips component to composition API + TS

* refactor(ui): move Chips component to composition API + TS
2025-02-21 18:59:19 +00:00
Calum H.
a02eb5445b Update Last Modified in cmp-info (#3287)
Signed-off-by: Calum H. <hendersoncal117@gmail.com>
2025-02-21 18:45:55 +00:00
Erb3
195cc9cee0 refactor(ui): move Toggle component to Composition API + TS (#3281)
* refactor(frontend): move Toggle component to Composition API + TS

**Toggle.vue**:
- Enable composition API and TS
- Added `disabled` to props
- Remove redundant `checked`
- Replace `modelValue` and `emits` with `defineModel` compiler macro

**Others**:
- Replace emit handling and `model-value` with `v-model` where simple logic was used
  - Not `FeatureFlagSettings.vue` (contained custom code on receiving emit)
  - Not `Mods.vue` (contained custom code on receiving emit)
- Remove redundant `checked` attribute

* fix(app): toggles not updating value
2025-02-21 18:43:49 +00:00
worldwidepixel
719b395b7b feat(frontend): Sort collections by creation date on user pages and dashboard (#3286) 2025-02-21 18:41:25 +00:00
Prospector
27fba4ba11 Fix padding error on revenue page (#3285) 2025-02-21 10:06:27 +00:00
Prospector
6667b620d1 Update changelog 2025-02-20 18:10:49 -08:00
Calum H.
c77f3395b2 feat(frontend): Improve revenue information (#3250)
* Improve revenue information

* Improve NET 60 period info + show next period if current period is over.

* invert period check

* %

* Finalize changes

* Cleanup

* Remove .idea

* Discard changes to .idea/discord.xml

* Discard changes to .idea/code.iml

* Discard changes to .idea/.gitignore

* Discard changes to .idea/libraries/KotlinJavaRuntime.xml

* Discard changes to .idea/vcs.xml

* Discard changes to .idea/modules.xml

* Discard changes to .idea/.gitignore

* fix lint issues

* table fix, lint fix and media sizing fix

* fix responsiveness

* Remove comment

* utc comment

* fix lint
2025-02-21 01:52:10 +00:00
Jai Agrawal
067f471766 Segment pending revenue in API response (#3283) 2025-02-20 23:07:54 +00:00
Prospector
f75d824c92 Add minimum height to content to fix space below the footer (#3279) 2025-02-20 22:38:28 +00:00
Prospector
c4f582e35b Fixed proof form styling in checklist (#3278) 2025-02-20 22:38:10 +00:00
Prospector
a8727d9a68 Updated changelog with Feb 19 update 2025-02-19 22:14:27 -08:00
Prospector
423dae5208 2025 :) 2025-02-19 22:10:15 -08:00
Prospector
dc7d8d6018 Add settings button to navbar 2025-02-19 22:08:16 -08:00
Prospector
db09ded836 New footer (#2988)
* Update footer to new design

* Update footer with changelog and security notice

* Move mastodon icon

* Make full-width instead of card

* Intl extract, lint
2025-02-19 22:01:29 -08:00
Prospector
df33ff7f60 Servers Marketing: Add Dallas location to globe, change copy to refer to states (#3272) 2025-02-19 22:01:11 -08:00
Prospector
ca63c09a0d Enhance moderation checklist (#3273) 2025-02-19 22:00:52 -08:00
Erb3
9c2cd868a7 refactor(frontend): Project page composition API + TS (#3245)
* refactor(frontend): move project description to composition API + TS

* refactor(frontend): rename to `patchRequestPayload` for consistency

* chore: lint
2025-02-19 18:49:07 +00:00
Jeffrey Daniel
f6d64e8fde Fix MessageBanner padding (#3271)
The MessageBanner padding that appears when a project is archived only has padding on the bottom and not the top. This means that there is no visual gap and it does follow the visual style of the rest of the website. I have fixed it.
2025-02-19 00:39:08 +00:00
Prospector
253e64884a Updated changelog with Servers Feb 18 release 2025-02-18 14:26:35 -08:00
Evan Song
a88593fec5 Modrinth Servers February Release: Bug Fix Round 1 (#3267)
* chore(pyroservers): attempt better error propogation

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(pyroservers): introduce deferred modules

* fix(pyroservers): synchronize server icon processing

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: server action buttons

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: bring back skeleton

* fix(startup): populate values on refresh

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: properly refresh network

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: do not open backup settings modal if fetch failed

* fix(platform): only clear selected loader version if selecting a different loader

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: parse links in console log

* fix: attempt to mitigate power button state flash

Signed-off-by: Evan Song <theevansong@gmail.com>

* Revert "fix: attempt to mitigate power button state flash"

This reverts commit 3ba5c0b4f7.

* refactor: error accumulation builder in PyroServersFetch

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(files): await deferred fs

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: startup border

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: prevent suspended server errors from being overwritten

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: add server id copy button to suspended server listing

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: refresh behavior

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: behavior of server icon in options

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: prevent error inspector failures from destroying the page

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: remove nexttick wrapper

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: ensure file edit gets initted due to deferred module

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: prevent module errors from breaking the layout

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

---------

Signed-off-by: Evan Song <theevansong@gmail.com>
2025-02-18 22:17:50 +00:00
Jai A
6c4548a303 add price id to active servers route 2025-02-16 21:42:16 -08:00
Prospector
4851f18079 Updated changelog 2025-02-16 19:04:11 -08:00
Jakob
bbb917c405 Fixed lack of space around verffication name in ConfirmModal (#3258)
Signed-off-by: Jakob <minenash@protonmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-02-16 18:59:26 -08:00
Prospector
81d34dfa86 Add stock checking to server upgrades (#3270)
* Add stock checking to server upgrades

* Fix route
2025-02-16 18:58:09 -08:00
Jai Agrawal
31723a2d3c Fix prorations not updating open charge (#3265) 2025-02-14 22:53:45 -08:00
Jai Agrawal
975427b319 Allow admins to view user email (#3261) 2025-02-13 17:18:03 -08:00
Prospector
d9983debce Change out of stock URL to discord (#3256)
* Change out of stock URL to discord, switch from router links to normal anchor tags

* Update changelog
2025-02-12 19:09:11 -08:00
Prospector
6a12ef0e2c Fix lint 2025-02-12 19:08:08 -08:00
Prospector
56ba342346 Initial servers upgrades frontend (#3219)
* Initial servers upgrades frontend

* Fix error when purchasing non-custom servers

* fix backend

* Fix comment

---------

Signed-off-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
2025-02-12 18:22:49 -08:00
Prospector
6d810a421a Servers marketing enhancements (#3252)
* feat: locations page + stock callouts

* feat: misalligned but spirits there!!

* fix readability on colors on globe

* Enhancements to globe

* Fix out of stock indicator styling

* Start globe near US and slow speed

* Remove debug statement

* Switch from capacity to stock API

* Make custom use its own stock checker

* Fix lint, add changelog entries

---------

Co-authored-by: Elizabeth <checksum@pyro.host>
Co-authored-by: Lio <git@lio.cat>
2025-02-12 12:06:51 -08:00
Prospector
098519dea1 Fix changelog timestamps being an hour off 2025-02-12 10:42:03 -08:00
Prospector
7183b3d761 Fix some changelog issues, and broader mobile padding issues (#3246) 2025-02-11 12:46:34 -08:00
Prospector
0ac49d846f Add project issues link to report form if it exists (#3215) 2025-02-11 11:07:23 -08:00
Prospector
cade2c182c Add changelog page to website (#3242)
* Add changelog page to website

* Add pages for individual changelog entries that can be linked to

* Handle first case for individual page

* Add some more changelog entries, improve some spacing
2025-02-11 08:50:27 -08:00
Erb3
affeec82f0 License UI redesign + composition API (#3225)
* refactor(frontend): revamp license page

- Add more understandable UI
  - Field titles
  - Field description
- Use more semantically correct elements
  - Make paragraph not a label
- Rephrase some parts
- Fields no longer jump around
- Split SPDX-identifier and license name into two seperate fields, for readability
- Sort imports
- fmt

* feat(frontend): encourage license URL on custom license

* refactor(frontend): license page to composition + ts

- Move to Vue composition API
- Move to TypeScript
- Move away from vue-multiselect to the dropdown component
- Use `formatProjectType` function for typesafety
- Remove unused form error highlighting code
- Creating typings for built-in licenses
- Move standard licenses to licenses.ts util
  - There are other license-related utils I want to move there eventually
- Fix typo in Project license type definition

* chore(frontend): fmt

* chore(frontend): fmt

* feat(frontend): require URL and name for custom license

* refactor(frontend): give license or-later checkbox own row

* chore(frontend): fmt
2025-02-10 08:37:49 -08:00
Evan Song
a75538c093 Modrinth Servers Mega Features & Bug Fix-a-thon (#3222)
* fix(content): changing mod versions works again

* chore(assets): update pyro logo

* fix(properties): deprecate fetchconfigfile

* Revert "fix(content): changing mod versions works again"

This reverts commit d7c0d1196f.

* feat(files): ability to sort via column click

* chore(startup): update clunky wording

* feat(serverListing): server icons SSR friendly

* fix(servers): if archon fails, display err in listing

* chore(serverlisting): use pyroserver hook to init icon

* chore(servers): much more graceful reinstall

* fix(servers): tw warn

* fix(platform): correctly react when pack reinstalled

* fix(serversroot): explicitly import navigateTo

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(serverlabels): show skeleton instead of hiding

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(platform): install-aware controls

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor!(platform): rewrite platform page

* fix(platform): regression in autoselecting loader

* chore(platform): prefer version over project modification date

* fix(platform): permanent hang after initial mount

* chore(platform): do not silently fail and hang if modpack fails loading

* oops: remove hardcoded error causer

* fix(platform): switch modpack btn while installing doesnt need class

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(platform): adjust styling in version modal

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(platform): prevent changing project card style

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor(pyrodropdown): rewrite

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(pyrodropdown): do nopt use deprecated substr

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(network): sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor(terminal): initial batch

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): fulllog over fullscreen

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): fullscreen conflict with body scroll

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(terminal): init drag select

* feat(terminal): shift click support

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): double lines limit

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(terminal): copy button

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): protip style

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): improve styles

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(terminal): regex search

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): move icons to icons dir

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): improve drag select autoscroll inertia

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): cancel selection on right click

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): progblur and stb btn disappearing

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor(serverstats): power efficiency

* fix(subdomainlabel): correct tooltip terminology

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(preferences): users hide subdomain label

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(servers): clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): deselect lines on escape

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(serversidebar): type err

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(fileitem): vue server render type

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): disable pointer events on lines if scrolling

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): search result counts style

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): plural

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(terminal): view selection

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(terminal): show actively selected lines in scrollbar

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminallog): btn color

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(gamelabel): align to text

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(gamelabel): align to text

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(listing): remove deadcode

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(serverlisting): deprecated process.server

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(platform): correctly disable button

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(backups): do not allow backup creation during server installation

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(platform): flush stale currentversion data on successful install

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(gamelabel): fix gap

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(network): vaporize uppercase

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(info): vaporize uppercase

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(backups): style unification

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(backups): finalize style change

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(servers): catch pyro servers fetch errors during ssr

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(serverstats): ram as bytes graph now works

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(platform): unify attempts and refresh interval

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): input

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(servers): installing ticket + update available notice back in platform

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): dont add bg to scroll track

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): preserve whitespace

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(serversroot): unnest blurred icon query

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(serverstats): clamp memory usage to 100% no matter what

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(terminal): allow copy of single lines, show btn

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(terminal): animate copy>view transition

Signed-off-by: Evan Song <theevansong@gmail.com>

* init: search improvements

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: lint

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: change log modal title

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: hide fullscreen when selecting and cancel selection on clickout

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor(terminal): more reliable jumpToLine

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: search results separator

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: remove buggy isScrollable check

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: style

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: correctly store pos to make jump reliable

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: disparity between search/log dragselect

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: prevent propagation of click events when clicking on jump btn

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: switch selection strategies depending on terminal mode

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: smarter esc handling

Signed-off-by: Evan Song <theevansong@gmail.com>

* finalize

Signed-off-by: Evan Song <theevansong@gmail.com>

* run fix

* fix: ensure lines between cannot be selected

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: increase initial log batch to 256

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): click on scroll track should take user to new scroll position

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): update aria label for view selected logs btn

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

---------

Signed-off-by: Evan Song <theevansong@gmail.com>
2025-02-10 07:39:13 -08:00
he3als
037cc86c1f Server Content Tab Fixes & Improvements (#3230)
* fix cancel button on edit modal

* make hardcoded mod text dynamic for plugins

* fix files path when clicking an external plugin

* fix plugins path for file uploads

* improve friendly mod name logic

* fix toggling plugins

* update pyroServers content definitions

* install then remove for changing version

Reinstall isn't currently implemented properly

* make the edit dialog pretty

* make new admonition component

* fix warning admonition colour

* new edit version modal

* cleanup

* make latest version default

* final touches

* lint
2025-02-08 10:31:38 -08:00
Jai Agrawal
1e8d550e96 Format traces as bullet list (MOD-32) (#3220)
* Format traces as bullet list

* Fix fmt
2025-02-06 13:23:42 -08:00
Jai A
acc379d14d Add clean.io to app frames 2025-02-06 10:20:58 -08:00
Jai A
5bc63a5ad3 Ads window system blocker 2025-02-06 10:02:44 -08:00
Prospector
a8630e93bc Redesign report form and prevent duplicate reports (#3211)
* Redesign report form and prevent duplicate reports

* Fix lint

* Add malware evidence notice to report form

* Fix lint
2025-02-02 14:09:10 -08:00
594 changed files with 24395 additions and 11740 deletions

View File

@@ -1,3 +1,6 @@
# Windows has stack overflows when calling from Tauri, so we increase compiler size
[target.'cfg(windows)']
rustflags = ["-C", "link-args=/STACK:16777220"]
[build]
rustflags = ["--cfg", "tokio_unstable"]

View File

@@ -16,7 +16,7 @@ body:
id: version
attributes:
label: What version of the Modrinth App are you using?
description: Find this in ⚙️ Settings (bottom right) -> About -> App version.
description: Find this in ⚙️ Settings (bottom right) -> After Modrinth App (bottom left)
validations:
required: true
- type: dropdown

BIN
.github/assets/api_cover.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -7,11 +7,13 @@ on:
paths:
- .github/workflows/daedalus-docker.yml
- 'apps/daedalus_client/**'
- 'packages/daedalus/**'
pull_request:
types: [opened, synchronize]
paths:
- .github/workflows/daedalus-docker.yml
- 'apps/daedalus_client/**'
- 'packages/daedalus/**'
merge_group:
types: [checks_requested]

View File

@@ -38,8 +38,10 @@ jobs:
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
env:
SQLX_OFFLINE: true
with:
context: ./apps/labrinth
file: ./apps/labrinth/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View File

@@ -6,9 +6,11 @@ on:
tags:
- 'v*'
paths:
- .github/workflows/app-release.yml
- .github/workflows/theseus-release.yml
- 'apps/app/**'
- 'apps/app-frontend/**'
- 'apps/labrinth/src/common/**'
- 'apps/labrinth/Cargo.toml'
- 'packages/app-lib/**'
- 'packages/app-macros/**'
- 'packages/assets/**'

4
.idea/code.iml generated
View File

@@ -10,9 +10,11 @@
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/rust-common/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
</module>

1685
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ members = [
'./apps/labrinth',
'./apps/daedalus_client',
'./packages/daedalus',
'./packages/ariadne',
]
# Optimize for speed and reduce size on release builds
@@ -21,4 +22,4 @@ strip = true # Remove debug symbols
opt-level = 3
[patch.crates-io]
wry = { git = "https://github.com/modrinth/wry", rev ="cdbf938" }
wry = { git = "https://github.com/modrinth/wry", rev = "51907c6" }

View File

@@ -1,7 +1,7 @@
{
"name": "@modrinth/app-frontend",
"private": true,
"version": "0.9.2",
"version": "0.9.3",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -2,6 +2,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import {
UserIcon,
ArrowBigUpDashIcon,
CompassIcon,
DownloadIcon,
@@ -233,6 +234,9 @@ async function fetchCredentials() {
credentials.value = creds
}
const profileMenu = ref()
const isProfileMenuOpen = computed(() => profileMenu.value?.isOpen)
async function signIn() {
await login().catch(handleError)
await fetchCredentials()
@@ -410,26 +414,34 @@ function handleAuxClick(e) {
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<SettingsIcon />
</NavButton>
<ButtonStyled v-if="credentials" type="transparent" circular>
<OverflowMenu
:options="[
{
id: 'sign-out',
action: () => logOut(),
color: 'danger',
},
]"
direction="left"
>
<Avatar
:src="credentials.user.avatar_url"
:alt="credentials.user.username"
size="32px"
circle
/>
<template #sign-out> <LogOutIcon /> Sign out </template>
</OverflowMenu>
</ButtonStyled>
<OverflowMenu
v-if="credentials"
ref="profileMenu"
placement="right-end"
class="w-12 h-12 border-none cursor-pointer rounded-full flex items-center justify-center text-2xl transition-all button-animation"
:class="isProfileMenuOpen ? 'bg-button-bg' : 'bg-transparent hover:bg-button-bg'"
:options="[
{
id: 'profile',
action: () => router.push(`/user/${credentials.user.id}`),
},
{
id: 'sign-out',
action: () => logOut(),
color: 'danger',
},
]"
direction="left"
>
<Avatar
:src="credentials.user.avatar_url"
:alt="credentials.user.username"
size="32px"
circle
/>
<template #profile> <UserIcon /> Profile </template>
<template #sign-out> <LogOutIcon /> Sign out </template>
</OverflowMenu>
<NavButton v-else v-tooltip.right="'Sign in'" :to="() => signIn()">
<LogInIcon />
<template #label>Sign in</template>
@@ -473,7 +485,7 @@ function handleAuxClick(e) {
<RunningAppBar />
</Suspense>
</div>
<section v-if="!nativeDecorations" class="window-controls">
<section v-if="!nativeDecorations" class="window-controls" data-tauri-drag-region-exclude>
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
<MinimizeIcon />
</Button>
@@ -521,6 +533,16 @@ function handleAuxClick(e) {
width: 'calc(100% - var(--right-bar-width))',
}"
></div>
<div
v-if="criticalErrorMessage"
class="m-6 mb-0 flex flex-col border-red bg-bg-red rounded-2xl border-2 border-solid p-4 gap-1 font-semibold text-contrast"
>
<h1 class="m-0 text-lg font-extrabold">{{ criticalErrorMessage.header }}</h1>
<div
class="markdown-body text-primary"
v-html="renderString(criticalErrorMessage.body ?? '')"
></div>
</div>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
@@ -592,12 +614,6 @@ function handleAuxClick(e) {
<PromotionWrapper />
</template>
</div>
<div class="view">
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
<h1>{{ criticalErrorMessage.header }}</h1>
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
</div>
</div>
</div>
<URLConfirmModal ref="urlModal" />
<Notifications ref="notificationsWrapper" sidebar />
@@ -694,12 +710,23 @@ function handleAuxClick(e) {
.app-grid-navbar {
grid-area: nav;
// Fixes SVG scaling issues
filter: brightness(1.00001);
}
.app-grid-statusbar {
grid-area: status;
}
[data-tauri-drag-region] {
-webkit-app-region: drag;
}
[data-tauri-drag-region-exclude] {
-webkit-app-region: no-drag;
}
.app-contents {
position: absolute;
z-index: 1;
@@ -769,6 +796,7 @@ function handleAuxClick(e) {
height: 100%;
overflow: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
}
.app-contents::before {

View File

@@ -181,24 +181,26 @@ const maxInstancesPerRow = ref(1)
const maxProjectsPerRow = ref(1)
const calculateCardsPerRow = () => {
// Calculate how many cards fit in one row
const containerWidth = rows.value[0].clientWidth
// Convert container width from pixels to rem
const containerWidthInRem =
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
if (rows.value && rows.value[0]) {
// Calculate how many cards fit in one row
const containerWidth = rows.value[0].clientWidth
// Convert container width from pixels to rem
const containerWidthInRem =
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
if (maxInstancesPerRow.value < 5) {
maxInstancesPerRow.value *= 2
}
if (maxInstancesPerCompactRow.value < 5) {
maxInstancesPerCompactRow.value *= 2
}
if (maxProjectsPerRow.value < 3) {
maxProjectsPerRow.value *= 2
if (maxInstancesPerRow.value < 5) {
maxInstancesPerRow.value *= 2
}
if (maxInstancesPerCompactRow.value < 5) {
maxInstancesPerCompactRow.value *= 2
}
if (maxProjectsPerRow.value < 3) {
maxProjectsPerRow.value *= 2
}
}
}
@@ -207,13 +209,17 @@ const resizeObserver = ref(null)
onMounted(() => {
calculateCardsPerRow()
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
resizeObserver.value.observe(rowContainer.value)
if (rowContainer.value) {
resizeObserver.value.observe(rowContainer.value)
}
window.addEventListener('resize', calculateCardsPerRow)
})
onUnmounted(() => {
window.removeEventListener('resize', calculateCardsPerRow)
resizeObserver.value.unobserve(rowContainer.value)
if (rowContainer.value) {
resizeObserver.value.unobserve(rowContainer.value)
}
})
</script>

View File

@@ -1,7 +1,16 @@
<script setup>
import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets'
import {
CheckIcon,
DropdownIcon,
XIcon,
HammerIcon,
LogInIcon,
UpdatedIcon,
CopyIcon,
} from '@modrinth/assets'
import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue'
import { ButtonStyled, Collapsible } from '@modrinth/ui'
import { ref, computed } from 'vue'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleError } from '@/store/notifications.js'
import { handleSevereError } from '@/store/error.js'
@@ -13,6 +22,7 @@ import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref()
const error = ref()
const closable = ref(true)
const errorCollapsed = ref(false)
const title = ref('An error occurred')
const errorType = ref('unknown')
@@ -118,6 +128,26 @@ async function repairInstance() {
}
loadingRepair.value = false
}
const hasDebugInfo = computed(
() =>
errorType.value === 'directory_move' ||
errorType.value === 'minecraft_auth' ||
errorType.value === 'state_init' ||
errorType.value === 'no_loader_version',
)
const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error message.')
const copied = ref(false)
async function copyToClipboard(text) {
await navigator.clipboard.writeText(text)
copied.value = true
setTimeout(() => {
copied.value = false
}, 3000)
}
</script>
<template>
@@ -244,16 +274,9 @@ async function repairInstance() {
</div>
</template>
<template v-else>
{{ error.message ?? error }}
{{ debugInfo }}
</template>
<template
v-if="
errorType === 'directory_move' ||
errorType === 'minecraft_auth' ||
errorType === 'state_init' ||
errorType === 'no_loader_version'
"
>
<template v-if="hasDebugInfo">
<hr />
<p>
If nothing is working and you need help, visit
@@ -261,16 +284,39 @@ async function repairInstance() {
and start a chat using the widget in the bottom right and we will be more than happy to
assist! Make sure to provide the following debug information to the agent:
</p>
<details>
<summary>Debug information</summary>
{{ error.message ?? error }}
</details>
</template>
</div>
<div class="input-group push-right">
<a :href="supportLink" class="btn" @click="errorModal.hide()"><ChatIcon /> Get support</a>
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
<div class="flex items-center gap-2">
<ButtonStyled>
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
</ButtonStyled>
<ButtonStyled v-if="closable">
<button @click="errorModal.hide()"><XIcon /> Close</button>
</ButtonStyled>
<ButtonStyled v-if="hasDebugInfo">
<button :disabled="copied" @click="copyToClipboard(debugInfo)">
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
<template v-else> <CopyIcon /> Copy debug info </template>
</button>
</ButtonStyled>
</div>
<template v-if="hasDebugInfo">
<div class="bg-button-bg rounded-xl mt-2 overflow-clip">
<button
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
@click="errorCollapsed = !errorCollapsed"
>
<span class="text-contrast font-extrabold m-0">Debug information:</span>
<DropdownIcon
class="h-5 w-5 text-secondary transition-transform"
:class="{ 'rotate-180': !errorCollapsed }"
/>
</button>
<Collapsible :collapsed="errorCollapsed">
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre>
</Collapsible>
</div>
</template>
</div>
</ModalWrapper>
</template>

View File

@@ -151,7 +151,7 @@ const exportPack = async () => {
</div>
</div>
<div v-if="showingFiles" class="table-content">
<div v-for="[path, children] of folders" :key="path.name" class="table-row">
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
<div class="table-cell file-entry">
<div class="file-primary">
<Checkbox

View File

@@ -2,14 +2,14 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import {
DownloadIcon,
GameIcon,
PlayIcon,
SpinnerIcon,
StopCircleIcon,
GameIcon,
TimerIcon,
StopCircleIcon,
PlayIcon,
DownloadIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import { ButtonStyled, Avatar, SmartClickable } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { finish_install, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
@@ -134,22 +134,26 @@ onUnmounted(() => unlisten())
</script>
<template>
<template v-if="compact">
<div
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
@click="seeInstance"
@mouseenter="checkProcess"
>
<SmartClickable class="card-shadow bg-bg-raised rounded-xl" @mouseenter="checkProcess">
<template #clickable>
<router-link
class="no-click-animation"
:to="`/instance/${encodeURIComponent(instance.path)}/`"
/>
</template>
<div v-if="compact" class="grid grid-cols-[auto_1fr_auto] p-3 pl-4 gap-2">
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.path"
alt="Mod card"
/>
<div class="h-full flex items-center font-bold text-contrast leading-normal">
<div
class="h-full flex items-center font-bold text-contrast leading-normal smart-clickable:underline-on-hover"
>
<span class="line-clamp-2">{{ instance.name }}</span>
</div>
<div class="flex items-center">
<div class="flex items-center smart-clickable:allow-pointer-events">
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
<StopCircleIcon />
@@ -176,13 +180,7 @@ onUnmounted(() => unlisten())
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
</div>
</div>
</template>
<div v-else>
<div
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
@click="seeInstance"
@mouseenter="checkProcess"
>
<div v-else class="p-4 rounded-xl flex gap-3 group" @mouseenter="checkProcess">
<div class="relative flex items-center justify-center">
<Avatar
size="48px"
@@ -231,7 +229,9 @@ onUnmounted(() => unlisten())
</div>
</div>
<div class="flex flex-col gap-1">
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
<p
class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1 smart-clickable:underline-on-hover"
>
{{ instance.name }}
</p>
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
@@ -242,5 +242,5 @@ onUnmounted(() => unlisten())
</div>
</div>
</div>
</div>
</SmartClickable>
</template>

View File

@@ -3,23 +3,18 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { formatCategory } from '@modrinth/utils'
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
type Instance = {
game_version: string
loader: string
path: string
install_stage: string
icon_path?: string
name: string
}
import type { GameInstance } from '@/helpers/types'
defineProps<{
instance: Instance
instance?: GameInstance
}>()
</script>
<template>
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
<div
v-if="instance"
class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4"
>
<router-link
:to="`/instance/${encodeURIComponent(instance.path)}`"
tabindex="-1"
@@ -49,5 +44,3 @@ defineProps<{
</ButtonStyled>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,16 +1,13 @@
<script setup>
import { Avatar, TagItem } from '@modrinth/ui'
import { Avatar, SmartClickable, TagItem } from '@modrinth/ui'
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
import { formatNumber, formatCategory } from '@modrinth/utils'
import { computed } from 'vue'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRouter } from 'vue-router'
dayjs.extend(relativeTime)
const router = useRouter()
const props = defineProps({
project: {
type: Object,
@@ -40,29 +37,15 @@ const toColor = computed(() => {
const r = (color >>> 16) & 0xff
return 'rgba(' + [r, g, b, 1].join(',') + ')'
})
const toTransparent = computed(() => {
let color = props.project.color
color >>>= 0
const b = color & 0xff
const g = (color >>> 8) & 0xff
const r = (color >>> 16) & 0xff
return (
'linear-gradient(rgba(' +
[r, g, b, 0.03].join(',') +
'), 65%, rgba(' +
[r, g, b, 0.3].join(',') +
'))'
)
})
</script>
<template>
<div
<SmartClickable
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
@click="router.push(`/project/${project.slug}`)"
>
<template #clickable>
<router-link class="no-click-animation" :to="`/project/${project.slug}`" />
</template>
<div
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
:style="{
@@ -73,21 +56,13 @@ const toTransparent = computed(() => {
'https://launcher-files.modrinth.com/assets/maze-bg.png'
})`,
}"
>
<div
class="badges-wrapper"
:class="{
'no-image': !project.featured_gallery && !project.gallery[0],
}"
:style="{
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
}"
></div>
</div>
></div>
<div class="flex flex-col justify-center gap-2 px-4 py-3">
<div class="flex gap-2 items-center">
<Avatar size="48px" :src="project.icon_url" />
<div class="h-full flex items-center font-bold text-contrast leading-normal">
<div
class="h-full flex items-center font-bold text-contrast leading-normal smart-clickable:underline-on-hover"
>
<span class="line-clamp-2">{{ project.title }}</span>
</div>
</div>
@@ -115,7 +90,7 @@ const toTransparent = computed(() => {
</div>
</div>
</div>
</div>
</SmartClickable>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,194 @@
<script setup lang="ts">
import {
VersionIcon,
ImageIcon,
BookmarkIcon,
DownloadIcon,
HeartIcon,
MoreVerticalIcon,
ExternalIcon,
LinkIcon,
ReportIcon,
SpinnerIcon,
CheckIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, OverflowMenu } from '@modrinth/ui'
import type { GameInstance } from '@/helpers/types'
import { computed, ref, type Ref } from 'vue'
import { install as installVersion } from '@/store/install'
import { useVIntl, defineMessages } from '@vintl/vintl'
import { isSearchResult, type Project, type SearchResult } from '@modrinth/utils'
import type { InstanceContentMap } from '@/composables/instance-context.ts'
const { formatMessage } = useVIntl()
const props = defineProps<{
project: Project | SearchResult
instance?: GameInstance
instanceContent?: InstanceContentMap
}>()
const installing = ref(false)
const installed: Ref<boolean> = ref(false)
function checkInstallStatus() {
if (props.instanceContent) {
installed.value = Object.values(props.instanceContent).some(
(content) => content.metadata?.project_id === projectId.value,
)
}
}
async function install(toInstance: boolean) {
if (toInstance) {
installing.value = true
}
await installVersion(
projectId.value,
null,
props.instance && toInstance ? props.instance.path : null,
'SearchCard',
() => {
if (toInstance) {
installing.value = false
installed.value = true
}
},
)
}
const modpack = computed(() => props.project.project_type === 'modpack')
const projectWebUrl = computed(
() => `https://modrinth.com/${props.project.project_type}/${props.project.slug}`,
)
const tooltip = defineMessages({
installing: {
id: 'project.card.actions.installing.tooltip',
defaultMessage: 'This project is being installed',
},
installed: {
id: 'project.card.actions.installed.tooltip',
defaultMessage: 'This project is already installed',
},
})
const messages = defineMessages({
viewVersions: {
id: 'project.card.actions.view-versions',
defaultMessage: 'View versions',
},
viewGallery: {
id: 'project.card.actions.view-gallery',
defaultMessage: 'View gallery',
},
})
const projectId = computed(() =>
isSearchResult(props.project) ? props.project.project_id : props.project.id,
)
const copyText = (text: string) => {
navigator.clipboard.writeText(text)
}
checkInstallStatus()
</script>
<template>
<ButtonStyled color="brand">
<button
v-tooltip="
installing
? formatMessage(tooltip.installing)
: installed
? formatMessage(tooltip.installed)
: null
"
:disabled="installing || installed"
@click="() => install(true)"
>
<SpinnerIcon v-if="installing" />
<CheckIcon v-else-if="installed" />
<DownloadIcon v-else />
{{
formatMessage(
installing
? commonMessages.installingButton
: installed
? commonMessages.installedButton
: commonMessages.installButton,
)
}}
</button>
</ButtonStyled>
<!-- TODO: Add in later -->
<ButtonStyled v-if="false" circular>
<button v-tooltip="'Follow'">
<HeartIcon />
</button>
</ButtonStyled>
<ButtonStyled v-if="false" circular>
<button v-tooltip="'Save'">
<BookmarkIcon />
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<OverflowMenu
:options="[
{
id: 'install-elsewhere',
color: 'primary',
action: () => install(false),
shown: !!instance && !modpack,
},
{
divider: true,
shown: !!instance && !modpack,
},
{
id: 'versions',
link: `/project/${projectId}/versions`,
},
{
id: 'gallery',
link: `/project/${projectId}/gallery`,
shown: (project.gallery?.length ?? 0) > 0,
},
{
id: 'open-link',
link: projectWebUrl,
},
{
id: 'copy-link',
action: () => copyText(projectWebUrl),
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
action: () => {},
},
]"
>
<MoreVerticalIcon />
<template #install-elsewhere>
<DownloadIcon /> {{ formatMessage(commonMessages.installToButton) }}
</template>
<template #versions> <VersionIcon /> {{ formatMessage(messages.viewVersions) }} </template>
<template #gallery> <ImageIcon /> {{ formatMessage(messages.viewGallery) }} </template>
<template #open-link>
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
</template>
<template #copy-link>
<LinkIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
</template>
<template #report> <ReportIcon /> {{ formatMessage(commonMessages.reportButton) }} </template>
</OverflowMenu>
</ButtonStyled>
</template>

View File

@@ -1,157 +1,72 @@
<template>
<div
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
@click="
() => {
emit('open')
$router.push({
path: `/project/${project.project_id ?? project.id}`,
<NewProjectCard
:project="project"
:link="
asLink(
{
path: `/project/${projectId}`,
query: { i: props.instance ? props.instance.path : undefined },
})
}
},
() => emit('open'),
)
"
:experimental-colors="themeStore.featureFlags.project_card_background"
:creator-link="
creator
? asLink(
{
path: `/user/${creator}`,
query: { i: props.instance ? props.instance.path : undefined },
},
() => emit('open'),
)
: undefined
"
>
<div class="icon w-[96px] h-[96px] relative">
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
</div>
<div class="flex flex-col gap-2 overflow-hidden">
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
{{ project.title }}
</span>
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
</div>
<div class="m-0 line-clamp-2">
{{ project.description }}
</div>
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
<TagsIcon class="h-4 w-4 shrink-0" />
<div
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
<template #actions>
<ButtonStyled color="brand">
<button
:disabled="installed || installing"
class="shrink-0 no-wrap"
@click.stop="install()"
>
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
Client or server
<template v-if="!installed">
<DownloadIcon />
</template>
<template
v-else-if="
(project.client_side === 'optional' || project.client_side === 'required') &&
(project.server_side === 'optional' || project.server_side === 'unsupported')
"
>
Client
</template>
<template
v-else-if="
(project.server_side === 'optional' || project.server_side === 'required') &&
(project.client_side === 'optional' || project.client_side === 'unsupported')
"
>
Server
</template>
<template
v-else-if="
project.client_side === 'unsupported' && project.server_side === 'unsupported'
"
>
Unsupported
</template>
<template
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
>
Client and server
</template>
</div>
<div
v-for="tag in categories"
:key="tag"
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
>
{{ formatCategory(tag.name) }}
</div>
</div>
</div>
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
<div class="flex items-center gap-2">
<DownloadIcon class="shrink-0" />
<span>
{{ formatNumber(project.downloads) }}
<span class="text-secondary">downloads</span>
</span>
</div>
<div class="flex items-center gap-2">
<HeartIcon class="shrink-0" />
<span>
{{ formatNumber(project.follows ?? project.followers) }}
<span class="text-secondary">followers</span>
</span>
</div>
<div class="mt-auto relative">
<div class="absolute bottom-0 right-0 w-fit">
<ButtonStyled color="brand" type="outlined">
<button
:disabled="installed || installing"
class="shrink-0 no-wrap"
@click.stop="install()"
>
<template v-if="!installed">
<DownloadIcon v-if="modpack || instance" />
<PlusIcon v-else />
</template>
<CheckIcon v-else />
{{
installing
? 'Installing'
: installed
? 'Installed'
: modpack || instance
? 'Install'
: 'Add to an instance'
}}
</button>
</ButtonStyled>
</div>
</div>
</div>
</div>
<CheckIcon v-else />
{{ installing ? 'Installing' : installed ? 'Installed' : 'Install' }}
</button>
</ButtonStyled>
</template>
</NewProjectCard>
</template>
<script setup>
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
import { ButtonStyled, Avatar } from '@modrinth/ui'
import { formatNumber, formatCategory } from '@modrinth/utils'
<script setup lang="ts">
import { DownloadIcon, CheckIcon } from '@modrinth/assets'
import { ButtonStyled, NewProjectCard, asLink } from '@modrinth/ui'
import type { Project, SearchResult } from '@modrinth/utils'
import { isSearchResult } from '@modrinth/utils'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { ref, computed } from 'vue'
import { install as installVersion } from '@/store/install.js'
import { useTheming } from '@/store/state.js'
import type { GameInstance } from '@/helpers/types'
dayjs.extend(relativeTime)
const props = defineProps({
backgroundImage: {
type: String,
default: null,
const themeStore = useTheming()
const props = withDefaults(
defineProps<{
project: Project | SearchResult
instance?: GameInstance
installed?: boolean
}>(),
{
instance: undefined,
installed: false,
},
project: {
type: Object,
required: true,
},
categories: {
type: Array,
required: true,
},
instance: {
type: Object,
default: null,
},
featured: {
type: Boolean,
default: false,
},
installed: {
type: Boolean,
default: false,
},
})
)
const emit = defineEmits(['open', 'install'])
@@ -160,16 +75,19 @@ const installing = ref(false)
async function install() {
installing.value = true
await installVersion(
props.project.project_id ?? props.project.id,
projectId.value,
null,
props.instance ? props.instance.path : null,
'SearchCard',
() => {
installing.value = false
emit('install', props.project.project_id ?? props.project.id)
emit('install', projectId.value)
},
)
}
const modpack = computed(() => props.project.project_type === 'modpack')
const projectId = computed(() =>
isSearchResult(props.project) ? props.project.project_id : props.project.id,
)
const creator = computed(() => (isSearchResult(props.project) ? props.project.author : undefined))
</script>

View File

@@ -5,7 +5,7 @@ import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { get } from '@/helpers/settings'
import { edit } from '@/helpers/profile'
import type { InstanceSettingsTabProps, AppSettings } from '../../../helpers/types'
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
const { formatMessage } = useVIntl()
@@ -114,7 +114,6 @@ const messages = defineMessages({
<Toggle
id="fullscreen"
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
:checked="fullscreenSetting"
:disabled="!overrideWindowSettings"
@update:model-value="
(e) => {

View File

@@ -116,10 +116,6 @@ function devModeCount() {
themeStore.devMode = !themeStore.devMode
settings.value.developer_mode = !!themeStore.devMode
devModeCounter.value = 0
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
modal.value.setTab(0)
}
}
}
</script>

View File

@@ -43,7 +43,7 @@ function onModalHide() {
if (props.showAdOnClose) {
show_ads_window()
}
props.onHide()
props.onHide?.()
}
</script>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { Toggle, ThemeSelector, TeleportDropdownMenu } from '@modrinth/ui'
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
import { useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings'
import { watch, ref } from 'vue'
import { ref, watch } from 'vue'
import { getOS } from '@/helpers/utils'
const themeStore = useTheming()
@@ -46,7 +46,6 @@ watch(
<Toggle
id="advanced-rendering"
:model-value="themeStore.advancedRendering"
:checked="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = e
@@ -61,16 +60,7 @@ watch(
<h2 class="m-0 text-lg font-extrabold text-contrast">Native Decorations</h2>
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
</div>
<Toggle
id="native-decorations"
:model-value="settings.native_decorations"
:checked="settings.native_decorations"
@update:model-value="
(e) => {
settings.native_decorations = e
}
"
/>
<Toggle id="native-decorations" v-model="settings.native_decorations" />
</div>
<div class="mt-4 flex items-center justify-between">
@@ -78,16 +68,7 @@ watch(
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
</div>
<Toggle
id="minimize-launcher"
:model-value="settings.hide_on_process_start"
:checked="settings.hide_on_process_start"
@update:model-value="
(e) => {
settings.hide_on_process_start = e
}
"
/>
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
</div>
<div class="mt-4 flex items-center justify-between">
@@ -111,7 +92,6 @@ watch(
<Toggle
id="toggle-sidebar"
:model-value="settings.toggle_sidebar"
:checked="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = e

View File

@@ -57,16 +57,7 @@ watch(
</p>
</div>
<Toggle
id="fullscreen"
:model-value="settings.force_fullscreen"
:checked="settings.force_fullscreen"
@update:model-value="
(e) => {
settings.force_fullscreen = e
}
"
/>
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
</div>
<div class="flex items-center justify-between gap-4">

View File

@@ -1,19 +1,22 @@
<script setup lang="ts">
import { Toggle } from '@modrinth/ui'
import { ButtonStyled, Toggle } from '@modrinth/ui'
import { useTheming } from '@/store/state'
import { ref, watch } from 'vue'
import { ref, type Ref, watch } from 'vue'
import { get, set } from '@/helpers/settings'
import { DEFAULT_FEATURE_FLAGS } from '@/store/theme'
type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
const themeStore = useTheming()
const settings = ref(await get())
const options = ref(['project_background', 'page_path'])
const options: Ref<FeatureFlag[]> = ref(Object.keys(DEFAULT_FEATURE_FLAGS) as FeatureFlag[])
function getStoreValue(key: string) {
function getStoreValue(key: FeatureFlag) {
return themeStore.featureFlags[key] ?? false
}
function setStoreValue(key: string, value: boolean) {
function setStoreValue(key: FeatureFlag, value: boolean) {
themeStore.featureFlags[key] = value
settings.value.feature_flags[key] = value
}
@@ -27,18 +30,39 @@ watch(
)
</script>
<template>
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
<h2 class="m-0 text-lg font-extrabold text-contrast">Feature flags</h2>
<p class="mt-1 mb-0 leading-tight text-secondary">
These are developer tools that are not intended to be used by end users except for debugging
purposes.
</p>
<p class="my-3 font-bold">Do not report bugs or issues if you have any feature flags enabled.</p>
<div
v-for="option in options"
:key="option"
class="mt-2 px-4 py-3 flex items-center justify-between bg-bg rounded-2xl"
>
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
{{ option }}
<h2 class="m-0 text-base font-bold text-primary capitalize">
{{ option.replace(new RegExp('_', 'g'), ' ') }}
</h2>
<p class="m-0 text-sm text-secondary">Default: {{ DEFAULT_FEATURE_FLAGS[option] }}</p>
</div>
<Toggle
id="advanced-rendering"
:model-value="getStoreValue(option)"
:checked="getStoreValue(option)"
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
/>
<div class="flex items-center gap-1">
<ButtonStyled type="transparent">
<button
class="text-sm"
:disabled="getStoreValue(option) === DEFAULT_FEATURE_FLAGS[option]"
@click="() => setStoreValue(option, !themeStore.featureFlags[option])"
>
Reset to default
</button>
</ButtonStyled>
<Toggle
id="advanced-rendering"
:model-value="getStoreValue(option)"
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
/>
</div>
</div>
</template>

View File

@@ -30,16 +30,7 @@ watch(
option, you opt out and ads will no longer be shown based on your interests.
</p>
</div>
<Toggle
id="personalized-ads"
:model-value="settings.personalized_ads"
:checked="settings.personalized_ads"
@update:model-value="
(e) => {
settings.personalized_ads = e
}
"
/>
<Toggle id="personalized-ads" v-model="settings.personalized_ads" />
</div>
<div class="mt-4 flex items-center justify-between gap-4">
@@ -51,16 +42,7 @@ watch(
longer be collected.
</p>
</div>
<Toggle
id="opt-out-analytics"
:model-value="settings.telemetry"
:checked="settings.telemetry"
@update:model-value="
(e) => {
settings.telemetry = e
}
"
/>
<Toggle id="opt-out-analytics" v-model="settings.telemetry" />
</div>
<div class="mt-4 flex items-center justify-between gap-4">
@@ -75,10 +57,6 @@ watch(
as those added by mods. (app restart required to take effect)
</p>
</div>
<Toggle
id="disable-discord-rpc"
v-model="settings.discord_rpc"
:checked="settings.discord_rpc"
/>
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
</div>
</template>

View File

@@ -0,0 +1,41 @@
import { useRoute } from 'vue-router'
import { ref, computed, type Ref, watch } from 'vue'
import { handleError } from '@/store/notifications'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
import type { GameInstance, InstanceContent } from '@/helpers/types'
export type InstanceContentMap = Record<string, InstanceContent>
export async function useInstanceContext() {
const route = useRoute()
const instance: Ref<GameInstance | undefined> = ref()
const instanceContent: Ref<InstanceContentMap | undefined> = ref()
await loadInstance()
watch(route, () => {
loadInstance()
})
async function loadInstance() {
;[instance.value, instanceContent.value] = await Promise.all([
route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(),
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
])
}
const instanceQueryAppendage = computed(() => {
if (instance.value) {
return `?i=${instance.value.path}`
} else {
return ''
}
})
return {
instance,
instanceContent,
instanceQueryAppendage,
}
}

View File

@@ -32,6 +32,18 @@ type GameInstance = {
hooks: Hooks
}
type InstanceContent = {
hash: string
file_name: string
size: number
metadata?: {
project_id: ModrinthId
version_id: ModrinthId
}
update_version_id: string
project_type: 'mod' | 'resourcepack' | 'datapack' | 'shaderpack'
}
type InstallStage =
| 'installed'
| 'minecraft_installing'

View File

@@ -20,6 +20,9 @@
"app.settings.tabs.resource-management": {
"message": "Resource management"
},
"instance.filter.disabled": {
"message": "Disabled projects"
},
"instance.filter.updates-available": {
"message": "Updates available"
},
@@ -305,6 +308,18 @@
"instance.settings.title": {
"message": "Settings"
},
"project.card.actions.installed.tooltip": {
"message": "This project is already installed"
},
"project.card.actions.installing.tooltip": {
"message": "This project is being installed"
},
"project.card.actions.view-gallery": {
"message": "View gallery"
},
"project.card.actions.view-versions": {
"message": "View versions"
},
"search.filter.locked.instance": {
"message": "Provided by the instance"
},

View File

@@ -2,7 +2,14 @@
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import type { Ref } from 'vue'
import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets'
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
import type {
CategoryTag,
GameVersionTag,
PlatformTag,
ProjectType,
SortType,
Tags,
} from '@modrinth/ui'
import {
SearchFilterControl,
SearchSidebarFilter,
@@ -19,14 +26,14 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import type { LocationQuery } 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'
import { useInstanceContext } from '@/composables/instance-context.ts'
import type { SearchResult } from '@modrinth/utils'
const { formatMessage } = useVIntl()
@@ -38,62 +45,45 @@ const projectTypes = computed(() => {
})
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories().catch(handleError).then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
get_categories()
.catch(handleError)
.then((x: CategoryTag[]) => ref(x)),
get_loaders()
.catch(handleError)
.then((x: PlatformTag[]) => ref(x)),
get_game_versions()
.catch(handleError)
.then((x: GameVersionTag[]) => ref(x)),
])
const tags: Ref<Tags> = computed(() => ({
gameVersions: availableGameVersions.value as GameVersion[],
loaders: loaders.value as Platform[],
categories: categories.value as Category[],
gameVersions: availableGameVersions.value as GameVersionTag[],
loaders: loaders.value as PlatformTag[],
categories: categories.value as CategoryTag[],
}))
type Instance = {
game_version: string
loader: string
path: string
install_stage: string
icon_path?: string
name: string
}
type InstanceProject = {
metadata: {
project_id: string
}
}
const instance: Ref<Instance | null> = ref(null)
const instanceProjects: Ref<InstanceProject[] | null> = ref(null)
const instanceHideInstalled = ref(false)
const newlyInstalled = ref([])
const newlyInstalled: Ref<string[]> = ref([])
const { instance, instanceContent } = await useInstanceContext()
const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
await updateInstanceContext()
await checkHideInstalledQuery()
watch(route, () => {
updateInstanceContext()
watch(instance, () => {
checkHideInstalledQuery()
})
async function updateInstanceContext() {
if (route.query.i) {
;[instance.value, instanceProjects.value] = await Promise.all([
getInstance(route.query.i).catch(handleError),
getInstanceProjects(route.query.i).catch(handleError),
])
newlyInstalled.value = []
}
async function checkHideInstalledQuery() {
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
instanceHideInstalled.value = route.query.ai === 'true'
}
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
instance.value = null
instanceHideInstalled.value = false
}
// if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
// instance.value = undefined
// instanceHideInstalled.value = false
// }
}
const instanceFilters = computed(() => {
@@ -119,10 +109,10 @@ const instanceFilters = computed(() => {
})
}
if (instanceHideInstalled.value && instanceProjects.value) {
const installedMods = Object.values(instanceProjects.value)
if (instanceHideInstalled.value && instanceContent.value) {
const installedMods: string[] = Object.values(instanceContent.value)
.filter((x) => x.metadata)
.map((x) => x.metadata.project_id)
.map((x) => x.metadata!.project_id)
installedMods.push(...newlyInstalled.value)
@@ -173,23 +163,27 @@ breadcrumbs.setContext({ name: 'Discover content', link: route.path, query: rout
const loading = ref(true)
const projectType = ref(route.params.projectType)
const projectType: Ref<ProjectType | undefined> = ref(
typeof route.params.projectType === 'string'
? (route.params.projectType as ProjectType)
: undefined,
)
watch(projectType, () => {
loading.value = true
})
type SearchResult = {
project_id: string
type ExtendedSearchResult = SearchResult & {
installed?: boolean
}
type SearchResults = {
total_hits: number
limit: number
hits: SearchResult[]
hits: ExtendedSearchResult[]
}
const results: Ref<SearchResults | null> = shallowRef(null)
const results: Ref<SearchResults | undefined> = shallowRef()
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
)
@@ -200,7 +194,7 @@ watch(requestParams, () => {
})
async function refreshSearch() {
let rawResults = await get_search_results(requestParams.value)
let rawResults = (await get_search_results(requestParams.value)) as { result: SearchResults }
if (!rawResults) {
rawResults = {
result: {
@@ -211,13 +205,15 @@ async function refreshSearch() {
}
}
if (instance.value) {
for (const val of rawResults.result.hits) {
val.installed =
newlyInstalled.value.includes(val.project_id) ||
Object.values(instanceProjects.value).some(
(x) => x.metadata && x.metadata.project_id === val.project_id,
)
}
rawResults.result.hits.map((x) => ({
...x,
installed:
newlyInstalled.value.includes(x.project_id) ||
(instanceContent.value &&
Object.values(instanceContent.value).some(
(content) => content.metadata && content.metadata.project_id === x.project_id,
)),
}))
}
results.value = rawResults.result
@@ -271,9 +267,9 @@ watch(
() => route.params.projectType,
async (newType) => {
// Check if the newType is not the same as the current value
if (!newType || newType === projectType.value) return
if (!newType || newType === projectType.value || typeof newType !== 'string') return
projectType.value = newType
projectType.value = newType as ProjectType
currentSortType.value = { display: 'Relevance', name: 'relevance' }
query.value = ''
@@ -287,7 +283,7 @@ const selectableProjectTypes = computed(() => {
if (instance.value) {
if (
availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <=
availableGameVersions.value.findIndex((x) => x.version === instance.value?.game_version) <=
availableGameVersions.value.findIndex((x) => x.version === '1.13')
) {
dataPacks = true
@@ -353,9 +349,10 @@ const messages = defineMessages({
},
})
const options = ref(null)
const handleRightClick = (event, result) => {
options.value.showMenu(event, result, [
const options: Ref<InstanceType<typeof ContextMenu> | null> = ref(null)
const handleRightClick = (event: MouseEvent, result: ExtendedSearchResult) => {
options.value?.showMenu(event, result, [
{
name: 'open_link',
},
@@ -364,7 +361,7 @@ const handleRightClick = (event, result) => {
},
])
}
const handleOptionsClick = (args) => {
const handleOptionsClick = (args: { item: ExtendedSearchResult; option: string }) => {
switch (args.option) {
case 'open_link':
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
@@ -477,33 +474,26 @@ await refreshSearch()
<section v-if="loading" class="offline">
<LoadingIndicator />
</section>
<section v-else-if="offline && results.total_hits === 0" class="offline">
<section v-else-if="offline && (!results || results.total_hits === 0)" class="offline">
You are currently offline. Connect to the internet to browse Modrinth!
</section>
<section v-else class="project-list display-mode--list instance-results" role="list">
<section
v-else-if="results"
class="project-list display-mode--list instance-results"
role="list"
>
<SearchCard
v-for="result in results.hits"
:key="result?.project_id"
:project="result"
:instance="instance"
:categories="[
...categories.filter(
(cat) =>
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
),
...loaders.filter(
(loader) =>
result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes(projectType),
),
]"
:installed="result.installed || newlyInstalled.includes(result.project_id)"
@install="
(id) => {
newlyInstalled.push(id)
}
"
@contextmenu.prevent.stop="(event) => handleRightClick(event, result)"
@contextmenu.prevent.stop="(event: MouseEvent) => handleRightClick(event, result)"
/>
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>

View File

@@ -0,0 +1,136 @@
<template>
<Teleport to="#sidebar-teleport-target">
<CollectionSidebarDescription
v-if="collection"
:collection="collection"
class="project-sidebar-section"
/>
<CollectionSidebarCurator
v-if="curator"
:user="curator"
:link="`/user/${curator.id}`"
class="project-sidebar-section"
/>
<CollectionSidebarDetails
v-if="collection"
:collection="collection"
class="project-sidebar-section"
/>
</Teleport>
<div v-if="collection" class="p-6 flex flex-col gap-4">
<InstanceIndicator :instance="instance" />
<CollectionHeader :collection="collection">
<template #actions>
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
<OverflowMenu
:options="[{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode }]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</CollectionHeader>
<div v-if="projects">
<ProjectsList
:projects="projects"
:project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`"
:experimental-colors="themeStore.featureFlags.project_card_background"
>
<template #project-actions="{ project }">
<ProjectCardActions
:instance="instance"
:instance-content="instanceContent"
:project="project"
/>
</template>
</ProjectsList>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, type Ref, watch } from 'vue'
import { handleError } from '@/store/notifications.js'
import {
ProjectsList,
ButtonStyled,
commonMessages,
OverflowMenu,
CollectionHeader,
CollectionSidebarCurator,
CollectionSidebarDescription,
CollectionSidebarDetails,
} from '@modrinth/ui'
import { ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
import { useVIntl } from '@vintl/vintl'
import { useFetch } from '@/helpers/fetch'
import type { User, Project, Collection } from '@modrinth/utils'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/theme'
import { useInstanceContext } from '@/composables/instance-context'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
const breadcrumbs = useBreadcrumbs()
const route = useRoute()
const { formatMessage } = useVIntl()
const collection: Ref<Collection | null> = ref(null)
const curator: Ref<User | null> = ref(null)
const projects: Ref<Project[]> = ref([])
async function fetchCollection() {
collection.value = await useFetch(
`https://api.modrinth.com/v3/collection/${route.params.id}`,
).catch(handleError)
if (!collection.value) {
return
}
;[projects.value, curator.value] = await Promise.all([
useFetch(
`https://api.modrinth.com/v2/projects?ids=${encodeURIComponent(JSON.stringify(collection.value.projects))}`,
),
useFetch(`https://api.modrinth.com/v2/user/${collection.value.user}`).catch(handleError),
])
breadcrumbs.setContext({ name: 'Collection', link: `/collection/${collection.value.name}` })
breadcrumbs.setName('Collection', collection.value.name)
}
await fetchCollection()
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/collection')) {
await fetchCollection()
}
},
)
const themeStore = useTheming()
async function copyId() {
if (collection.value) {
await navigator.clipboard.writeText(String(collection.value.id))
}
}
</script>
<style scoped lang="scss">
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
}
</style>

View File

@@ -0,0 +1,3 @@
import Index from './Index.vue'
export { Index }

View File

@@ -17,11 +17,11 @@
<div
class="flex items-center gap-2 font-semibold transform capitalize border-0 border-solid border-divider pr-4 md:border-r"
>
<GameIcon class="h-6 w-6 text-secondary" />
<GameIcon class="h-5 w-5 text-secondary" />
{{ instance.loader }} {{ instance.game_version }}
</div>
<div class="flex items-center gap-2 font-semibold">
<TimerIcon class="h-6 w-6 text-secondary" />
<TimerIcon class="h-5 w-5 text-secondary" />
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>

View File

@@ -66,10 +66,13 @@
if (x.author) {
item.creator = {
name: x.author,
type: 'user',
id: x.author,
link: 'https://modrinth.com/user/' + x.author,
name: x.author.name,
type: x.author.type,
id: x.author.slug,
link: {
path: `/${x.author.type}/${x.author.slug}`,
query: { i: props.instance.path },
},
linkProps: { target: '_blank' },
}
}
@@ -179,7 +182,6 @@
<Toggle
class="!mx-2"
:model-value="!item.data.disabled"
:checked="!item.data.disabled"
@update:model-value="toggleDisableMod(item.data)"
/>
<ButtonStyled type="transparent" circular>
@@ -330,6 +332,28 @@ const props = defineProps({
},
})
type ProjectListEntryAuthor = {
name: string
slug: string
type: 'user' | 'organization'
}
type ProjectListEntry = {
path: string
name: string
slug?: string
author: ProjectListEntryAuthor | null
version: string | null
file_name: string
icon: string | null
disabled: boolean
updateVersion?: string
outdated: boolean
updated: dayjs.Dayjs
project_type: string
id?: string
}
const isPackLocked = computed(() => {
return props.instance.linked_data && props.instance.linked_data.locked
})
@@ -339,7 +363,7 @@ const canUpdatePack = computed(() => {
})
const exportModal = ref(null)
const projects = ref([])
const projects = ref<ProjectListEntry[]>([])
const selectedFiles = ref([])
const selectedProjects = computed(() =>
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
@@ -348,7 +372,7 @@ const selectedProjects = computed(() =>
const selectionMap = ref(new Map())
const initProjects = async (cacheBehaviour?) => {
const newProjects = []
const newProjects: ProjectListEntry[] = []
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
const fetchProjects = []
@@ -385,21 +409,29 @@ const initProjects = async (cacheBehaviour?) => {
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
let owner
let author: ProjectListEntryAuthor | null
if (org) {
owner = org.name
author = {
name: org.name,
slug: org.slug,
type: 'organization',
}
} else if (team) {
owner = team.find((x) => x.is_owner).user.username
const teamMember = team.find((x) => x.is_owner)
author = {
name: teamMember.user.username,
slug: teamMember.user.username,
type: 'user',
}
} else {
owner = null
author = null
}
newProjects.push({
path,
name: project.title,
slug: project.slug,
author: owner,
author,
version: version.version_number,
file_name: file.file_name,
icon: project.icon_url,
@@ -418,7 +450,7 @@ const initProjects = async (cacheBehaviour?) => {
newProjects.push({
path,
name: file.file_name.replace('.disabled', ''),
author: '',
author: null,
version: null,
file_name: file.file_name,
icon: null,

View File

@@ -10,7 +10,7 @@ defineProps({
</script>
<template>
<GridDisplay
v-if="instances.length > 0"
v-if="instances && instances.length > 0"
label="Instances"
:instances="instances.filter((i) => !i.linked_data)"
/>

View File

@@ -10,7 +10,7 @@ defineProps({
</script>
<template>
<GridDisplay
v-if="instances.length > 0"
v-if="instances && instances.length > 0"
label="Instances"
:instances="instances.filter((i) => i.linked_data)"
/>

View File

@@ -9,5 +9,5 @@ defineProps({
})
</script>
<template>
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
<GridDisplay v-if="instances && instances.length > 0" label="Instances" :instances="instances" />
</template>

View File

@@ -0,0 +1,176 @@
<template>
<Teleport to="#sidebar-teleport-target">
<OrganizationSidebarMembers
v-if="organization"
:members="organization.members"
:user-link="(user) => `/user/${user.id}`"
class="project-sidebar-section"
/>
</Teleport>
<div v-if="organization" class="flex flex-col gap-4 p-6">
<InstanceIndicator :instance="instance" />
<OrganizationHeader
:organization="organization"
:download-count="sumDownloads"
:project-count="projects.length"
>
<template #actions>
<ButtonStyled v-if="themeStore.devMode" circular type="transparent" size="large">
<OverflowMenu
:options="[{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode }]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</OrganizationHeader>
<div v-if="projects">
<ProjectsList
:projects="projects"
:project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`"
:experimental-colors="themeStore.featureFlags.project_card_background"
>
<template #project-actions="{ project }">
<ProjectCardActions
:project="project"
:instance="instance"
:instance-content="instanceContent"
/>
</template>
</ProjectsList>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, type Ref, watch, computed } from 'vue'
import { handleError } from '@/store/notifications.js'
import {
ProjectsList,
ButtonStyled,
commonMessages,
OverflowMenu,
OrganizationHeader,
OrganizationSidebarMembers,
} from '@modrinth/ui'
import { ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
import { useVIntl } from '@vintl/vintl'
import { useFetch } from '@/helpers/fetch'
import type { Project, Organization, ProjectV3, Environment } from '@modrinth/utils'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/theme'
import { useInstanceContext } from '@/composables/instance-context.ts'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
const breadcrumbs = useBreadcrumbs()
const route = useRoute()
const { formatMessage } = useVIntl()
const organization: Ref<Organization | null> = ref(null)
const projects: Ref<Project[]> = ref([])
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
async function fetchOrganization() {
organization.value = await useFetch(
`https://api.modrinth.com/v3/organization/${route.params.id}`,
).catch(handleError)
projects.value = (
await useFetch(`https://api.modrinth.com/v3/organization/${route.params.id}/projects`).catch(
handleError,
)
).map((projectV3: ProjectV3) => {
let type = projectV3.project_types[0]
if (type === 'plugin' || type === 'datapack') {
type = 'mod'
}
let clientSide: Environment = 'unknown'
let serverSide: Environment = 'unknown'
const singleplayer = projectV3.singleplayer && projectV3.singleplayer[0]
const clientAndServer = projectV3.client_and_server && projectV3.client_and_server[0]
const clientOnly = projectV3.client_only && projectV3.client_only[0]
const serverOnly = projectV3.server_only && projectV3.server_only[0]
// quick and dirty hack to show envs as legacy
if (singleplayer && clientAndServer && !clientOnly && !serverOnly) {
clientSide = 'required'
serverSide = 'required'
} else if (singleplayer && clientAndServer && clientOnly && !serverOnly) {
clientSide = 'required'
serverSide = 'unsupported'
} else if (singleplayer && clientAndServer && !clientOnly && serverOnly) {
clientSide = 'unsupported'
serverSide = 'required'
} else if (singleplayer && clientAndServer && clientOnly && serverOnly) {
clientSide = 'optional'
serverSide = 'optional'
}
const projectV2: Project = {
...projectV3,
title: projectV3.name,
description: projectV3.summary,
body: projectV3.description,
project_type: type,
team: projectV3.team_id,
donation_urls: [],
client_side: clientSide,
server_side: serverSide,
}
return projectV2
})
if (!organization.value) {
return
}
breadcrumbs.setName('Organization', organization.value.name)
}
await fetchOrganization()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/organization')) {
await fetchOrganization()
}
},
)
const themeStore = useTheming()
async function copyId() {
if (organization.value) {
await navigator.clipboard.writeText(String(organization.value.id))
}
}
const sumDownloads = computed(() => {
let sum = 0
for (const project of projects.value) {
sum += project.downloads
}
return sum
})
</script>
<style scoped lang="scss">
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
}
</style>

View File

@@ -0,0 +1,3 @@
import Index from './Index.vue'
export { Index }

View File

@@ -31,10 +31,10 @@
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@click.stop=""
@click.stop="() => {}"
/>
<div class="floating" @click.stop="">
<div class="floating" @click.stop="() => {}">
<div class="text">
<h2 v-if="expandedGalleryItem.title">
{{ expandedGalleryItem.title }}
@@ -99,7 +99,7 @@ import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const props = defineProps({
project: {
type: Object,
default: () => {},
default: () => ({}),
},
})

View File

@@ -8,11 +8,10 @@
/>
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
<ProjectSidebarCreators
:organization="null"
:organization="organization"
:members="members"
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
:user-link="(username) => `https://modrinth.com/user/${username}`"
link-target="_blank"
:org-link="(org) => `/organization/${org.id}${instance ? '?i=' + instance.path : ''}`"
:user-link="(user) => `/user/${user.id}${instance ? '?i=' + instance.path : ''}`"
class="project-sidebar-section"
/>
<ProjectSidebarDetails
@@ -23,7 +22,7 @@
/>
</Teleport>
<div class="flex flex-col gap-4 p-6">
<InstanceIndicator v-if="instance" :instance="instance" />
<InstanceIndicator :instance="instance" />
<template v-if="data">
<Teleport
v-if="themeStore.featureFlags.project_background"
@@ -92,6 +91,11 @@
label: 'Description',
href: `/project/${$route.params.id}`,
},
{
label: 'Gallery',
href: `/project/${$route.params.id}/gallery`,
shown: data.gallery.length > 0,
},
{
label: 'Versions',
href: {
@@ -100,11 +104,6 @@
},
subpages: ['version'],
},
{
label: 'Gallery',
href: `/project/${$route.params.id}/gallery`,
shown: data.gallery.length > 0,
},
]"
/>
<RouterView
@@ -166,6 +165,7 @@ 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'
import { useFetch } from '@/helpers/fetch.js'
dayjs.extend(relativeTime)
@@ -177,6 +177,7 @@ const installing = ref(false)
const data = shallowRef(null)
const versions = shallowRef([])
const members = shallowRef([])
const organization = shallowRef(null)
const categories = shallowRef([])
const instance = ref(null)
const instanceProjects = ref(null)
@@ -202,6 +203,12 @@ async function fetchProjectData() {
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
])
if (project.organization) {
organization.value = await useFetch(
`https://api.modrinth.com/v3/organization/${project.organization}`,
).catch(handleError)
}
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
if (instanceProjects.value) {
@@ -213,6 +220,7 @@ async function fetchProjectData() {
installedVersion.value = installedFile.metadata.version_id
}
}
breadcrumbs.setName('Project', data.value.title)
}
@@ -437,6 +445,10 @@ const handleOptionsClick = (args) => {
}
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<Teleport to="#sidebar-teleport-target">
<UserSidebarOrganizations
:organizations="organizations"
:link="(org: Organization) => `/organization/${org.id}${instanceQueryAppendage}`"
class="project-sidebar-section"
/>
<UserSidebarBadges
v-if="user"
:user="user"
:download-count="sumDownloads"
class="project-sidebar-section"
/>
<UserSidebarCollections
:collections="collections"
:link="(collection: Collection) => `/collection/${collection.id}${instanceQueryAppendage}`"
class="project-sidebar-section"
/>
</Teleport>
<div v-if="user" class="p-6 flex flex-col gap-4">
<InstanceIndicator :instance="instance" />
<UserHeader :user="user" :project-count="projects.length" :download-count="sumDownloads">
<template #actions>
<ButtonStyled circular type="transparent" size="large">
<OverflowMenu
:options="[
{
id: 'report',
link: `https://modrinth.com/report?item=user&itemID=${user.id}`,
color: 'red',
},
{ id: 'copy-id', action: () => copyId(), shown: themeStore.devMode },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</UserHeader>
<div v-if="projects">
<ProjectsList
:projects="projects"
:project-link="(project) => `/project/${project.id}${instanceQueryAppendage}`"
:experimental-colors="themeStore.featureFlags.project_card_background"
>
<template #project-actions="{ project }">
<ProjectCardActions
:instance="instance"
:instance-content="instanceContent"
:project="project"
/>
</template>
</ProjectsList>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, type Ref, watch, computed } from 'vue'
import { handleError } from '@/store/notifications.js'
import {
ProjectsList,
UserSidebarOrganizations,
ButtonStyled,
commonMessages,
OverflowMenu,
UserHeader,
UserSidebarBadges,
UserSidebarCollections,
} from '@modrinth/ui'
import { ReportIcon, ClipboardCopyIcon, MoreVerticalIcon } from '@modrinth/assets'
import { useVIntl } from '@vintl/vintl'
import { useFetch } from '@/helpers/fetch'
import type { User, Project, Organization, Collection } from '@modrinth/utils'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/theme'
import { useInstanceContext } from '@/composables/instance-context'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import ProjectCardActions from '@/components/ui/ProjectCardActions.vue'
const breadcrumbs = useBreadcrumbs()
const route = useRoute()
const { formatMessage } = useVIntl()
const user: Ref<User | null> = ref(null)
const projects: Ref<Project[]> = ref([])
const organizations: Ref<Organization[]> = ref([])
const collections: Ref<Collection[]> = ref([])
async function fetchUser() {
;[user.value, projects.value, organizations.value, collections.value] = await Promise.all([
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}`).catch(handleError),
useFetch(`https://api.modrinth.com/v2/user/${route.params.id}/projects`).catch(handleError),
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/organizations`).catch(
handleError,
),
useFetch(`https://api.modrinth.com/v3/user/${route.params.id}/collections`).catch(handleError),
])
if (!user.value) {
return
}
breadcrumbs.setContext({ name: 'User', link: `/user/${user.value.username}` })
breadcrumbs.setName('User', user.value.username)
}
await fetchUser()
const { instance, instanceContent, instanceQueryAppendage } = await useInstanceContext()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/user')) {
await fetchUser()
}
},
)
const themeStore = useTheming()
async function copyId() {
if (user.value) {
await navigator.clipboard.writeText(String(user.value.id))
}
}
const sumDownloads = computed(() => {
let sum = 0
for (const project of projects.value) {
sum += project.downloads
}
return sum
})
</script>
<style scoped lang="scss">
.project-sidebar-section {
@apply p-4 flex flex-col gap-2 border-0 border-[--brand-gradient-border] border-solid;
}
.project-sidebar-section:not(:last-child) {
@apply border-b-[1px];
}
</style>

View File

@@ -0,0 +1,3 @@
import Index from './Index.vue'
export { Index }

View File

@@ -1,6 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router'
import * as Pages from '@/pages'
import * as Project from '@/pages/project'
import * as User from '@/pages/user'
import * as Organization from '@/pages/organization'
import * as Collection from '@/pages/collection'
import * as Instance from '@/pages/instance'
import * as Library from '@/pages/library'
@@ -100,6 +103,36 @@ export default new createRouter({
},
],
},
{
path: '/user/:id',
name: 'User',
component: User.Index,
props: true,
meta: {
useContext: true,
breadcrumb: [{ name: '?User', link: '/user/{id}' }],
},
},
{
path: '/organization/:id',
name: 'Organization',
component: Organization.Index,
props: true,
meta: {
useContext: true,
breadcrumb: [{ name: '?Organization', link: '/organization/{id}' }],
},
},
{
path: '/collection/:id',
name: 'Collection',
component: Collection.Index,
props: true,
meta: {
useContext: true,
breadcrumb: [{ name: '?Collection', link: '/collection/{id}' }],
},
},
{
path: '/instance/:id',
name: 'Instance',

View File

@@ -1,5 +1,11 @@
import { defineStore } from 'pinia'
export const DEFAULT_FEATURE_FLAGS = {
project_background: false,
page_path: false,
project_card_background: false,
}
export const useTheming = defineStore('themeStore', {
state: () => ({
themeOptions: ['dark', 'light', 'oled', 'system'],
@@ -8,7 +14,7 @@ export const useTheming = defineStore('themeStore', {
toggleSidebar: false,
devMode: false,
featureFlags: {},
featureFlags: DEFAULT_FEATURE_FLAGS,
}),
actions: {
setThemeState(newTheme) {

View File

@@ -0,0 +1,2 @@
[env]
SQLX_OFFLINE = "true"

View File

@@ -7,18 +7,7 @@ edition = "2021"
[dependencies]
theseus = { path = "../../packages/app-lib", features = ["cli"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
url = "2.2"
webbrowser = "0.8.13"
dunce = "1.0.3"
futures = "0.3"
uuid = { version = "1.1", features = ["serde", "v4"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.18"
tracing-error = "0.2.0"

View File

@@ -3,9 +3,9 @@
windows_subsystem = "windows"
)]
use std::time::Duration;
use theseus::prelude::*;
use theseus::profile::create::profile_create;
use tokio::signal::ctrl_c;
// A simple Rust implementation of the authentication run
// 1) call the authenticate_begin_flow() function to get the URL to open (like you would in the frontend)
@@ -41,54 +41,21 @@ async fn main() -> theseus::Result<()> {
// Initialize state
State::init().await?;
if minecraft_auth::users().await?.is_empty() {
println!("No users found, authenticating.");
authenticate_run().await?; // could take credentials from here direct, but also deposited in state users
}
//
// st.settings
// .write()
// .await
// .java_globals
// .insert(JAVA_8_KEY.to_string(), check_jre(path).await?.unwrap());
// Clear profiles
println!("Clearing profiles.");
{
let h = profile::list().await?;
for profile in h.into_iter() {
profile::remove(&profile.path).await?;
loop {
if State::get().await?.friends_socket.is_connected().await {
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
println!("Creating/adding profile.");
tracing::info!("Starting host");
let name = "Example".to_string();
let game_version = "1.16.1".to_string();
let modloader = ModLoader::Forge;
let loader_version = "stable".to_string();
let socket = State::get().await?.friends_socket.open_port(25565).await?;
tracing::info!("Running host on socket {}", socket.socket_id());
let profile_path = profile_create(
name,
game_version,
modloader,
Some(loader_version),
None,
None,
None,
)
.await?;
println!("running");
// Run a profile, running minecraft and store the RwLock to the process
let process = profile::run(&profile_path).await?;
println!("Minecraft UUID: {}", process.uuid);
println!("All running process UUID {:?}", process::get_all().await?);
// hold the lock to the process until it ends
println!("Waiting for process to end...");
process::wait_for(process.uuid).await?;
ctrl_c().await?;
tracing::info!("Stopping host");
socket.shutdown().await?;
Ok(())
}

View File

@@ -0,0 +1,2 @@
[env]
SQLX_OFFLINE = "true"

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus_gui"
version = "0.9.2"
version = "0.9.3"
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
license = "GPL-3.0-only"
repository = "https://github.com/modrinth/code/apps/app/"
@@ -28,12 +28,9 @@ tauri-plugin-single-instance = { version = "2.2.0" }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
futures = "0.3"
daedalus = { path = "../../packages/daedalus" }
chrono = "0.4.26"
dirs = "5.0.1"
url = "2.2"
uuid = { version = "1.1", features = ["serde", "v4"] }
os_info = "3.7.0"
@@ -41,9 +38,6 @@ os_info = "3.7.0"
tracing = "0.1.37"
tracing-error = "0.2.0"
lazy_static = "1"
once_cell = "1"
dashmap = "6.0.1"
paste = "1.0.15"

View File

@@ -39,12 +39,12 @@
"fileAssociations": [
{
"ext": ["mrpack"],
"mimeType": "application/zip+mrpack"
"mimeType": "application/x-modrinth-modpack+zip"
}
]
},
"productName": "Modrinth App",
"version": "0.9.2",
"version": "0.9.3",
"mainBinaryName": "Modrinth App",
"identifier": "ModrinthApp",
"plugins": {

View File

@@ -22,7 +22,6 @@ reqwest = { version = "0.12.5", default-features = false, features = [
"rustls-tls-native-roots",
] }
async_zip = { version = "0.0.17", features = ["full"] }
semver = "1.0"
chrono = { version = "0.4", features = ["serde"] }
bytes = "1.6.0"
rust-s3 = { version = "0.33.0", default-features = false, features = [
@@ -39,4 +38,3 @@ tracing-error = "0.2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-futures = { version = "0.2.5", features = ["futures", "tokio"] }

View File

@@ -598,7 +598,7 @@ async fn fetch(
))
})?;
let file_name = value.split('/').last()
let file_name = value.split('/').next_back()
.ok_or_else(|| {
crate::ErrorKind::InvalidInput(format!(
"Unable reading filename for data key {key} at path {value}",

View File

@@ -44,6 +44,10 @@ export default defineConfig({
label: 'Contributing to Modrinth',
autogenerate: { directory: 'contributing' },
},
{
label: 'Guides',
autogenerate: { directory: 'guide' },
},
// Add the generated sidebar group to the sidebar.
...openAPISidebarGroups,
],

View File

@@ -10,12 +10,12 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.3",
"@astrojs/starlight": "^0.26.3",
"@astrojs/check": "^0.9.4",
"@astrojs/starlight": "^0.32.2",
"@modrinth/assets": "workspace:*",
"astro": "^4.10.2",
"sharp": "^0.32.5",
"starlight-openapi": "^0.7.0",
"typescript": "^5.5.4"
"astro": "^5.4.1",
"sharp": "^0.33.5",
"starlight-openapi": "^0.14.0",
"typescript": "^5.8.2"
}
}
}

View File

@@ -1,7 +1,7 @@
openapi: '3.0.0'
info:
version: v2.7.0/15cf3fc
version: v2.7.0/366f528
title: Labrinth
termsOfService: https://modrinth.com/legal/terms
contact:
@@ -51,35 +51,7 @@ info:
Please note that certain scopes and requests cannot be completed with a personal access token or using OAuth.
For example, deleting a user account can only be done through Modrinth's frontend.
### OAuth2
Applications interacting with an authenticated API should create an OAuth2 application.
You can do this in [the developer settings](https://modrinth.com/settings/applications).
Make sure to save your application secret, as you will not be able to access it after you leave the page.
Once you have created a client, use the following URL to have a user authorize your client:
```
https://modrinth.com/auth/authorize?client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=<SCOPE_ONE>+<SCOPE_TWO>+<SCOPE_THREE>
```
> You can get a list of all scope names [here](https://github.com/modrinth/code/tree/main/apps/labrinth/src/models/v3/pats.rs).
Then, send a `POST` request to the following URL to get the token:
```
https://api.modrinth.com/_internal/oauth/token
```
> Note that you will need to provide your application's secret under the Authorization header.
In the body of your request, make sure to include the following:
- `code`: The code generated when authorizing your client
- `client_id`: Your client ID (found in developer settings)
- `redirect_uri`: A valid redirect URI provided in your application's settings
- `grant_type`: This will need to be `authorization_code`.
If your token request fails for any reason, you will need to get another code from the authorization process.
This route will be changed in the future to move the `_internal` part to `v3`.
A detailed guide on OAuth has been published in [Modrinth's technical documentation](https://docs.modrinth.com/guide/oauth).
### Personal access tokens
Personal access tokens (PATs) can be generated in from [the user settings](https://modrinth.com/settings/account).
@@ -1823,7 +1795,7 @@ components:
description: Number of projects on Modrinth
versions:
type: integer
description: Number of projects on Modrinth
description: Number of versions on Modrinth
files:
type: integer
description: Number of version files on Modrinth
@@ -3018,6 +2990,24 @@ paths:
$ref: '#/components/schemas/InvalidInputError'
'404':
description: The requested item(s) were not found or no authorization to access the requested item(s)
delete:
summary: Remove user's avatar
operationId: deleteUserIcon
tags:
- users
security:
- TokenAuth: ['USER_WRITE']
responses:
'204':
description: Expected response to a valid request
'400':
description: Request was invalid, see given error
content:
application/json:
schema:
$ref: '#/components/schemas/InvalidInputError'
'404':
description: The requested item(s) were not found or no authorization to access the requested item(s)
/user/{id|username}/projects:
parameters:
- $ref: '#/components/parameters/UserIdentifier'

View File

@@ -0,0 +1,7 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};

View File

@@ -1,6 +0,0 @@
import { defineCollection } from 'astro:content'
import { docsSchema } from '@astrojs/starlight/schema'
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
}

View File

@@ -0,0 +1,95 @@
---
title: The hitchhiker's guide to OAuth
description: Guide for using Modrinth OAuth to interact with the API on users' behalf.
---
Modrinth allows developers to create applications which, once authorized by a Modrinth user, let the developer interact with the API on their behalf. The flow used to get an API token is based on the OAuth 2 protocol. It is recommended that most people use an existing OAuth library to handle the authentication. If you want to implement it from scratch, you will need to look into [RFC 6749]. If the only user of the application is yourself, a personal access token (PAT) may be a better fit.
If you're familiar with OAuth 2, these are the URLs you will need:
| Name | URL |
|--------------------|--------------------------------------------------|
| Authorization page | `https://modrinth.com/auth/authorize` |
| Token exchange | `https://api.modrinth.com/_internal/oauth/token` |
The flow will generally look like this:
1. User is redirected to Modrinth to authorize your application
2. User is redirected back to your site after authorizing, with an authorization code
3. Your backend exchanges this code for an access token
## Register your application
To start off, you need to [register an application] in Modrinth's systems. The settings chosen here can always be changed later. You need to select what permissions you need, called scopes. For security reasons you will want to select only the scopes you need. See the [principle of least privilege].
In addition to name and scopes, you will also need to add one or more redirect URIs. These are the URIs that the user can be redirected to after they authorize your application.
After you've registered your application, it is important that you take note of the client secret somewhere safe. If the client secret is to ever leak, it is important that you regenerate it to ensure the security of your authorized users. If your client secret or access tokens are found exposed in the wild, your application may be disabled without prior notice.
## Getting authorization
Once the user is ready to authorize your application, you need to construct a URL to redirect them to. The authorization URL for Modrinth is `https://api.modrinth.com/_internal/oauth/token`. Supply the following query parameters:
| Query parameter | Description |
|-----------------|-------------------------------------------------------------------------------------------|
| `response_type` | In Modrinth this always needs to be `code`, since only code grants are supported |
| `client_id` | The application identifier found in the settings |
| `scope` | The permissions you need access to |
| `state` | A mechanism to prevent certain attacks. Explained further below. Recommended but optional |
| `redirect_uri` | The URI the user is redirect to after finishing authorization |
You might have noticed the `state` parameter. [CSRF] (Cross-site request forgery), and [clickjacking] are security vulnerabilities that you're recommended to protect against. In OAuth2 this is usually done with the `state` parameter. When the user initiates a request to start authorization, you include a `state` which is unique to this request. This can, for example, be saved in localStorge or a cookie. When the redirect URI is called, you verify that the `state` parameter is the same. Using `state` is optional, but recommended.
The scope identifiers are currently best found in the backend source code located at [`apps/labrinth/src/models/v3/pats.rs`]. The scope parameter is an array of scope identifiers, seperated by a plus sign (`+`).
The redirect URI is the endpoint on your server that will receive the code which can eventually be used to act on the user's behalf. For security reasons the redirect URI used has to be allowlisted in your application settings. The redirect will contain the following query parameters:
| Query parameter | Description |
|-----------------|----------------------------------------------------|
| `code` | The code that can be exchanged for an access token |
| `client_id` | Your client id |
| `redirect_uri` | The redirect URI which was used |
| `grant_type` | Always `authorization_code` in Modrinth |
## Exchanging tokens
If you've followed the previous section on getting authorization, you should now have an authorization code. Before you can access the API, you need to exchange this code for an access token. This is done by sending a POST request to the exchange token endpoint, `https://api.modrinth.com/_internal/oauth/token`. This request has to be of type urlencoded form. Make sure the `Content-Type` header is set to `application/x-www-form-urlencoded`. To authenticate this request you need to place your client secret in the `Authorization` header.
In the body use these fields:
| Field | Description |
|----------------|--------------------------------------------------------------|
| `code` | The authorization code |
| `client_id` | Your client id, the same as in the authorization request |
| `redirect_uri` | The redirect URI which was redirected to after authorization |
| `grant_type` | Always `authorization_code` in Modrinth |
If the request succeeds, you should receive a JSON payload with these fields:
| Field | Description |
|----------------|------------------------------------------------------|
| `access_token` | The access token you can use to access the API |
| `token_type` | Currently only `Bearer` |
| `expires_in` | The amount of seconds until the access token expires |
To use this access token, you attach it to API requests in the `Authorization` header. To get basic information about the authorizer, you can use the [`/user` endpoint], which automatically gets the user from the header.
If you have any questions, you're welcome to ask in #api-development in the [Discord guild], or create a ticket on the [support portal].
[RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
[register an application]: https://modrinth.com/settings/applications
[principle of least privilege]: https://en.wikipedia.org/wiki/Principle_of_least_privilege
[`apps/labrinth/src/models/v3/pats.rs`]: https://github.com/modrinth/code/blob/main/apps/labrinth/src/models/v3/pats.rs
[CSRF]: https://en.wikipedia.org/wiki/Cross-site_request_forgery
[Clickjacking]: https://en.wikipedia.org/wiki/Clickjacking
[`/user` endpoint]: https://docs.modrinth.com/api/operations/getuserfromauth/
[Discord guild]: https://discord.modrinth.com
[support portal]: https://support.modrinth.com/en/

View File

@@ -1,2 +1,3 @@
BASE_URL=https://api.modrinth.com/v2/
BROWSER_BASE_URL=https://api.modrinth.com/v2/
PYRO_BASE_URL=https://archon.modrinth.com/

View File

@@ -126,6 +126,7 @@ export default defineNuxtConfig({
homePageSearch?: any[];
homePageNotifs?: any[];
products?: any[];
errors?: number[];
} = {};
try {
@@ -157,6 +158,14 @@ export default defineNuxtConfig({
},
};
const caughtErrorCodes = new Set<number>();
function handleFetchError(err: any, defaultValue: any) {
console.error("Error generating state: ", err);
caughtErrorCodes.add(err.status);
return defaultValue;
}
const [
categories,
loaders,
@@ -168,15 +177,25 @@ export default defineNuxtConfig({
homePageNotifs,
products,
] = await Promise.all([
$fetch(`${API_URL}tag/category`, headers),
$fetch(`${API_URL}tag/loader`, headers),
$fetch(`${API_URL}tag/game_version`, headers),
$fetch(`${API_URL}tag/donation_platform`, headers),
$fetch(`${API_URL}tag/report_type`, headers),
$fetch(`${API_URL}projects_random?count=60`, headers),
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers),
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers),
$fetch(`${API_URL.replace("/v2/", "/_internal/")}billing/products`, headers),
$fetch(`${API_URL}tag/category`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/loader`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/game_version`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/donation_platform`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}tag/report_type`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}projects_random?count=60`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL.replace("/v2/", "/_internal/")}billing/products`, headers).catch((err) =>
handleFetchError(err, []),
),
]);
state.categories = categories;
@@ -188,6 +207,7 @@ export default defineNuxtConfig({
state.homePageSearch = homePageSearch;
state.homePageNotifs = homePageNotifs;
state.products = products;
state.errors = [...caughtErrorCodes];
await fs.writeFile("./src/generated/state.json", JSON.stringify(state));

View File

@@ -10,7 +10,7 @@
"postinstall": "nuxi prepare",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
},
"devDependencies": {
"@formatjs/cli": "^6.2.12",
@@ -57,6 +57,8 @@
"pinia": "^2.1.7",
"qrcode.vue": "^3.4.0",
"semver": "^7.5.4",
"three": "^0.172.0",
"@types/three": "^0.172.0",
"vue-multiselect": "3.0.0-alpha.2",
"vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4",

View File

@@ -1,6 +0,0 @@
<svg 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">
<line x1="21" y1="6" x2="3" y2="6"></line>
<line x1="15" y1="12" x2="3" y2="12"></line>
<line x1="17" y1="18" x2="3" y2="18"></line>
</svg>

Before

Width:  |  Height:  |  Size: 328 B

View File

@@ -1,6 +0,0 @@
<svg 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">
<rect x="2" y="4" width="20" height="5" rx="2"></rect>
<path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9"></path>
<path d="M10 13h4"></path>
</svg>

Before

Width:  |  Height:  |  Size: 335 B

View File

@@ -1,6 +0,0 @@
<svg 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">
<path d="M12 6v12"></path>
<path d="M17.196 9 6.804 15"></path>
<path d="m6.804 9 10.392 6"></path>
</svg>

Before

Width:  |  Height:  |  Size: 295 B

View File

@@ -1,7 +0,0 @@
<svg 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">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
<path d="M2 8c0-2.2.7-4.3 2-6"></path>
<path d="M22 8a10 10 0 0 0-2-6"></path>
</svg>

Before

Width:  |  Height:  |  Size: 383 B

View File

@@ -1,5 +0,0 @@
<svg 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">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>

Before

Width:  |  Height:  |  Size: 300 B

View File

@@ -1,6 +0,0 @@
<svg 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">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.29 7 12 12 20.71 7"></polyline>
<line x1="12" y1="22" x2="12" y2="12"></line>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

View File

@@ -1 +0,0 @@
<svg 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" class="lucide lucide-calendar-clock"><path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"/><path d="M16 2v4"/><path d="M8 2v4"/><path d="M3 10h5"/><path d="M17.5 17.5 16 16.25V14"/><path d="M22 16a6 6 0 1 1-12 0 6 6 0 0 1 12 0Z"/></svg>

Before

Width:  |  Height:  |  Size: 436 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8 7V3M16 7V3M7 11H17M5 21H19C20.1046 21 21 20.1046 21 19V7C21 5.89543 20.1046 5 19 5H5C3.89543 5 3 5.89543 3 7V19C3 20.1046 3.89543 21 5 21Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 349 B

View File

@@ -1,7 +0,0 @@
<svg 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">
<path d="M3 3v18h18"></path>
<path d="M18 17V9"></path>
<path d="M13 17V5"></path>
<path d="M8 17v-3"></path>
</svg>

Before

Width:  |  Height:  |  Size: 307 B

View File

@@ -1,4 +0,0 @@
<svg 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">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
<path d="m9 12 2 2 4-4"></path>
</svg>

Before

Width:  |  Height:  |  Size: 315 B

View File

@@ -1,4 +0,0 @@
<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20 6L9 17l-5-5" />
</svg>

Before

Width:  |  Height:  |  Size: 197 B

View File

@@ -1,4 +0,0 @@
<svg 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">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 238 B

View File

@@ -1 +0,0 @@
<svg 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"><polyline points="9 18 15 12 9 6"></polyline></svg>

Before

Width:  |  Height:  |  Size: 233 B

View File

@@ -1,5 +0,0 @@
<svg 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"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>

Before

Width:  |  Height:  |  Size: 323 B

View File

@@ -1,4 +0,0 @@
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>

Before

Width:  |  Height:  |  Size: 291 B

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /></svg>

Before

Width:  |  Height:  |  Size: 299 B

View File

@@ -1,7 +0,0 @@
<svg 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="8" cy="8" r="6"></circle>
<path d="M18.09 10.37A6 6 0 1 1 10.34 18"></path>
<path d="M7 6h1v4"></path>
<path d="m16.71 13.88.7.71-2.82 2.82"></path>
</svg>

Before

Width:  |  Height:  |  Size: 358 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24" xml:space="preserve"><path d="M9 5v4m0 0H5m4 0L4 4m11 1v4m0 0h4m-4 0 5-5M9 19v-4m0 0H5m4 0-5 5m11-5 5 5m-5-5v4m0-4h4" style="fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"/></svg>

Before

Width:  |  Height:  |  Size: 322 B

View File

@@ -1,5 +0,0 @@
<svg 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"></circle>
<path d="M15 9.354a4 4 0 1 0 0 5.292"></path>
</svg>

Before

Width:  |  Height:  |  Size: 280 B

View File

@@ -1,5 +0,0 @@
<svg 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">
<line x1="12" y1="2" x2="12" y2="22"></line>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>

Before

Width:  |  Height:  |  Size: 306 B

View File

@@ -1,7 +0,0 @@
<svg 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">
<rect x="3" y="3" width="7" height="9"></rect>
<rect x="14" y="3" width="7" height="5"></rect>
<rect x="14" y="12" width="7" height="9"></rect>
<rect x="3" y="16" width="7" height="5"></rect>
</svg>

Before

Width:  |  Height:  |  Size: 389 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>

Before

Width:  |  Height:  |  Size: 244 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 9L12 16L5 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 219 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11 5H6C4.89543 5 4 5.89543 4 7V18C4 19.1046 4.89543 20 6 20H17C18.1046 20 19 19.1046 19 18V13M17.5858 3.58579C18.3668 2.80474 19.6332 2.80474 20.4142 3.58579C21.1953 4.36683 21.1953 5.63316 20.4142 6.41421L11.8284 15H9L9 12.1716L17.5858 3.58579Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 454 B

View File

@@ -1 +0,0 @@
<svg 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"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 274 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>

Before

Width:  |  Height:  |  Size: 271 B

View File

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>

Before

Width:  |  Height:  |  Size: 348 B

View File

@@ -1 +0,0 @@
<svg 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"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>

Before

Width:  |  Height:  |  Size: 417 B

View File

@@ -1 +0,0 @@
<svg 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"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>

Before

Width:  |  Height:  |  Size: 275 B

View File

@@ -1,8 +0,0 @@
<svg 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" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 486 B

View File

@@ -1 +0,0 @@
<svg 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"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>

Before

Width:  |  Height:  |  Size: 320 B

View File

@@ -1 +0,0 @@
<svg 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" class="feather feather-filter"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>

Before

Width:  |  Height:  |  Size: 290 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="2" viewBox="0 0 14 2">
<path d="M18,12H6" transform="translate(-5 -11)" fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" />
</svg>

Before

Width:  |  Height:  |  Size: 242 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24" xml:space="preserve">
<path fill="currentColor" class="st0"
d="m12 1c-6.3 0-11.3 5-11.3 11.3 0 5 3.2 9.2 7.7 10.7 0.6 0.1 0.8-0.2 0.8-0.5v-1.9c-3.2 0.6-3.8-1.6-3.8-1.6-0.5-1.3-1.3-1.7-1.3-1.7-1-0.7 0.1-0.7 0.1-0.7 1.1 0.1 1.7 1.2 1.7 1.2 1 1.7 2.7 1.2 3.3 0.9 0.1-0.7 0.4-1.2 0.7-1.5-2.5-0.2-5.1-1.2-5.1-5.5 0-1.2 0.4-2.2 1.2-3-0.1-0.3-0.5-1.4 0.1-3 0 0 1-0.3 3.1 1.2 0.9-0.3 1.8-0.5 2.8-0.5s1.9 0.1 2.8 0.4c2.2-1.5 3.1-1.2 3.1-1.2 0.6 1.6 0.2 2.7 0.1 3 0.7 0.8 1.2 1.8 1.2 3 0 4.4-2.6 5.3-5.2 5.6 0.4 0.3 0.8 1 0.8 2.1v3.1c0 0.3 0.2 0.7 0.8 0.5 4.5-1.5 7.7-5.7 7.7-10.7 0-6.2-5-11.2-11.3-11.2z" />
</svg>

Before

Width:  |  Height:  |  Size: 717 B

View File

@@ -1,6 +0,0 @@
<svg 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"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 390 B

View File

@@ -1,7 +0,0 @@
<svg 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">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>

Before

Width:  |  Height:  |  Size: 389 B

View File

@@ -1,6 +0,0 @@
<svg 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" class="feather feather-menu">
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>

Before

Width:  |  Height:  |  Size: 352 B

View File

@@ -1 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="20" y2="15"></line><line x1="10" y1="3" x2="8" y2="21"></line><line x1="16" y1="3" x2="14" y2="21"></line></svg>

Before

Width:  |  Height:  |  Size: 303 B

Some files were not shown because too many files have changed in this diff Show More