@@ -19,4 +18,4 @@ export function ErrorScreen({error}: FallbackProps) {
export function onError(error: Error) {
LogError(`${error}`);
-}
\ No newline at end of file
+}
diff --git a/src/routes/Home/Home.module.css b/src/routes/Home/Home.module.css
index f09a0bd..0754218 100644
--- a/src/routes/Home/Home.module.css
+++ b/src/routes/Home/Home.module.css
@@ -1,81 +1,8 @@
-.banner {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
-
- background-color: #080A0E;
- height: 250px;
-
- color: white;
-}
-
-.banner > h1 {
- font-weight: 300;
-}
-
-.blue {
- color: var(--accent);
-}
-
.content {
- display: flex;
- padding: 25px;
- align-items: flex-start;
- gap: 25px;
- align-self: stretch;
- min-height: calc(100% - 250px);
-
- background: var(--white-background);
-}
-
-.content_inner {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 25px;
- flex: 1 0 0;
-}
-
-.sidebar {
- display: flex;
- width: 350px;
- flex-direction: column;
- align-items: flex-start;
- gap: 25px;
- align-self: stretch;
-}
-.discord_box {
- display: flex;
- padding: 25px;
- align-items: flex-start;
- gap: 10px;
- align-self: stretch;
-
- border-radius: 8px;
- background: #5561EF;
-
- color: #FFF;
- font-size: 20px;
- font-style: normal;
- font-weight: 700;
- line-height: normal;
- text-transform: uppercase;
-
- /* background-image: linear-gradient(133deg, #4458D1 45.69%, rgba(17, 52, 132, 0.00) 95.38%), url(/src/assets/DiscordBanner.png);
- background-position: 0, 115.562px -55.512px;
- background-size: auto, 114% 290.613%;
- background-repeat: no-repeat, no-repeat; */
-
- background-image: linear-gradient(90deg, #4458D1 40%, rgba(17, 52, 132, 0.00) 75%), url(/src/assets/Background.png);
- background-position: 0, 115.562px -55.512px;
- background-size: auto, 114% 290.613%;
- background-repeat: no-repeat, no-repeat;
+ padding: 24px;
}
-
-@media only screen and (max-width: 1100px) {
- .sidebar {
- display: none;
- }
-}
\ No newline at end of file
diff --git a/src/routes/Home/index.tsx b/src/routes/Home/index.tsx
index 771ad99..4e68cec 100644
--- a/src/routes/Home/index.tsx
+++ b/src/routes/Home/index.tsx
@@ -1,26 +1,10 @@
-import { DiscordIcon } from "@app/assets/Icons";
import styles from "./Home.module.css";
import NewsSection from "@app/components/NewsSection";
function Home() {
- return <>
-
-
Welcome to the YARC Launcher Beta
-
Here you can download and install YARG, and the official YARG setlist!
-
If you encounter any bugs, please report it to us in our Discord.
-
-
- >;
+ return
+
+ ;
}
-export default Home;
\ No newline at end of file
+export default Home;
diff --git a/src/routes/Marketplace/Marketplace.module.css b/src/routes/Marketplace/Marketplace.module.css
new file mode 100644
index 0000000..a797df3
--- /dev/null
+++ b/src/routes/Marketplace/Marketplace.module.css
@@ -0,0 +1,190 @@
+.main {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ flex: 1 0 0;
+}
+
+.banner {
+ display: flex;
+ height: 290px;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ align-self: stretch;
+ flex-direction: column;
+
+ background: linear-gradient(68deg, var(--accent) -29.28%, transparent 110.95%),
+ var(--banner) lightgray 50% / cover no-repeat;
+}
+
+.banner > .newTag {
+ display: flex;
+ padding: 5px 12px;
+ justify-content: center;
+ align-items: center;
+ gap: 5px;
+
+ border-radius: 50px;
+ background: #FCD548;
+ border: 2px solid #ffe071;
+ margin-bottom: 10px;
+
+ color: #4F2600;
+ font-size: 10px;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.banner > .preHeader {
+ color: rgba(15, 17, 24, 0.75);
+ font-family: "Archivo Black", var(--backupFonts);
+ font-size: 20px;
+ font-weight: 400;
+ text-transform: uppercase;
+}
+
+.banner > .header {
+ color: #0F1118;
+ font-family: "Big Shoulders Text", var(--backupFonts);
+ text-shadow: 4px 4px var(--accent);
+ line-height: 90%;
+ font-size: 120px;
+ font-weight: 400;
+ text-transform: uppercase;
+}
+
+.banner > .buttons {
+ margin-top: 12px;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.search {
+ display: flex;
+ height: 100px;
+ padding: 20px 0px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ align-self: stretch;
+}
+
+.content {
+ display: flex;
+ padding: 24px;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 24px;
+ align-self: stretch;
+}
+
+.section {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ align-self: stretch;
+}
+
+.section > .title {
+ display: flex;
+ padding: 12px 0px;
+ justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
+}
+
+.section > .title > .left {
+ display: flex;
+ width: 179px;
+ justify-content: space-between;
+ align-items: center;
+
+ color: #41475F;
+ font-size: 16px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.section > .list {
+ --item-width: 400px;
+
+ max-width: 100%;
+ overflow-x: auto;
+
+ padding: 12px;
+
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(var(--item-width), auto));
+ grid-gap: 12px;
+ grid-auto-flow: column;
+}
+
+.section.expanded > .list {
+ width: 100%;
+ grid-auto-flow: row;
+}
+
+.profileView {
+ display: flex;
+
+ min-width: var(--item-width);
+ max-width: var(--item-width);
+
+ padding: 12px;
+ align-items: center;
+ gap: 20px;
+
+ border: none;
+ border-radius: 16px;
+ background: linear-gradient(241deg, rgba(0, 0, 0, 0.00) 14.98%, #000 107.9%),
+ var(--background) lightgray 50% / cover no-repeat;
+
+ --outline-width: 2px;
+ outline: var(--outline-width) solid rgba(255, 255, 255, 0.15);
+ outline-offset: calc(var(--outline-width) * -1);
+
+ box-shadow: none;
+
+ cursor: pointer;
+ transition: transform 0.1s, box-shadow 0.3s;
+}
+
+.expanded .profileView {
+ max-width: none;
+}
+
+.profileView:hover {
+ box-shadow: 2px 2px 7px 0px rgba(0, 0, 0, 0.4);
+ transform: scale(1.025);
+}
+
+.profileView > .icon {
+ width: 100px;
+ height: 100px;
+}
+
+.profileView > .info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+
+ text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.6);
+}
+
+.profileView > .info > header {
+ color: #FFF;
+ font-size: 20px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.profileView > .info > span {
+ color: #C5C5C5;
+ font-size: 16px;
+ font-weight: 400;
+}
diff --git a/src/routes/Marketplace/MarketplacePopup.module.css b/src/routes/Marketplace/MarketplacePopup.module.css
new file mode 100644
index 0000000..f437ce9
--- /dev/null
+++ b/src/routes/Marketplace/MarketplacePopup.module.css
@@ -0,0 +1,131 @@
+.popup {
+ position: absolute;
+
+ top: var(--titleBarHeight);
+ bottom: 0;
+ left: var(--sideBarWidth);
+ right: 0;
+
+ background: rgba(0, 0, 0, 0.6);
+
+ z-index: 998;
+}
+
+.body {
+ position: absolute;
+
+ /* Masking prevents artifacts, unlike overflow: hidden */
+ mask: linear-gradient(#000 0 0);
+
+ top: 64px;
+ bottom: 64px;
+ left: 32px;
+ right: 32px;
+
+ border-radius: 16px;
+
+ background-color: #FFF;
+
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ flex: 1 0 0;
+ align-self: stretch;
+}
+
+.content {
+ display: flex;
+ padding: 16px;
+ align-items: flex-start;
+ gap: 10px;
+ flex-direction: column;
+
+ overflow: auto;
+}
+
+.close {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+
+ width: 16px;
+ height: 16px;
+
+ cursor: pointer;
+}
+
+.close > * {
+ color: #FFF;
+ filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 1));
+}
+
+.bannerContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ align-self: stretch;
+
+ background: linear-gradient(241deg, rgba(0, 0, 0, 0.00) 14.98%, #000 107.9%),
+ var(--bannerBack) lightgray 50% / cover no-repeat;
+}
+
+.bannerApp {
+ display: flex;
+ padding: 30px;
+ align-items: center;
+ gap: 25px;
+ align-self: stretch;
+}
+
+.bannerAppIcon {
+ width: 100px;
+ height: 100px;
+}
+
+.bannerApp > div {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 10px;
+
+ color: #FFF;
+ font-size: 30px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.verifiedTag {
+ display: flex;
+ height: 31px;
+ padding: 12px 5px 12px 10px;
+ justify-content: center;
+ align-items: center;
+ gap: 5px;
+
+ border-radius: 38px;
+ border: 1px solid rgba(255, 255, 255, 0.30);
+ background: rgba(0, 0, 0, 0.50);
+
+ color: #BDBDBD;
+ font-size: 15px;
+ font-weight: 700;
+}
+
+.bannerOptions {
+ display: flex;
+ padding: 15px;
+ padding-left: 25px;
+ justify-content: flex-end;
+ align-items: center;
+ align-self: stretch;
+
+ background: rgba(0, 0, 0, 0.75);
+ backdrop-filter: blur(5px);
+}
+
+.bannerOptionsMain {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
diff --git a/src/routes/Marketplace/MarketplacePopup.tsx b/src/routes/Marketplace/MarketplacePopup.tsx
new file mode 100644
index 0000000..32eabe3
--- /dev/null
+++ b/src/routes/Marketplace/MarketplacePopup.tsx
@@ -0,0 +1,125 @@
+import { MarketplaceProfile } from "@app/profiles/marketplace";
+import { useProfileStore } from "@app/profiles/store";
+import styles from "./MarketplacePopup.module.css";
+import { AddIcon, CloseIcon, InformationIcon, VerifiedIcon } from "@app/assets/Icons";
+import { localizeMetadata, processAssetUrl } from "@app/profiles/utils";
+import { useQuery } from "@tanstack/react-query";
+import { ApplicationMetadata, Profile } from "@app/profiles/types";
+import ProfileIcon from "@app/components/ProfileIcon";
+import Button, { ButtonColor } from "@app/components/Button";
+import { useNavigate } from "react-router-dom";
+import { useState } from "react";
+import Setlist from "@app/components/Setlist";
+import Box from "@app/components/Box";
+
+interface Props {
+ marketplaceProfile?: MarketplaceProfile,
+ setSelectedProfile: React.Dispatch
>,
+}
+
+const MarketplacePopup: React.FC = ({ marketplaceProfile, setSelectedProfile }: Props) => {
+ const profiles = useProfileStore();
+ const profileQuery = useQuery({
+ enabled: marketplaceProfile !== undefined,
+ queryKey: ["Profile", marketplaceProfile?.uuid],
+ queryFn: async (): Promise => await fetch((marketplaceProfile as MarketplaceProfile).url)
+ .then(res => res.json())
+ });
+
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+
+ if (marketplaceProfile === undefined || profileQuery.isLoading) {
+ return <>>;
+ }
+
+ const anyOfProfile = profiles.anyOfProfileUUID(marketplaceProfile.uuid);
+
+ const profile = profileQuery.data;
+ if (profileQuery.isError || profile === undefined) {
+ return <>
+ Error: {profileQuery.error}
+ >;
+ }
+
+ const metadata = localizeMetadata(profile, "en-US");
+
+ const addToLibrary = async () => {
+ if (loading) {
+ return;
+ }
+
+ setLoading(true);
+ const uuid = await profiles.activateProfile(marketplaceProfile.url);
+ setSelectedProfile(undefined);
+
+ if (uuid !== undefined) {
+ navigate(`/app-profile/${uuid}`);
+ }
+ };
+
+ let button;
+ if (loading) {
+ button = ;
+ } else if (!anyOfProfile) {
+ button = ;
+ } else if (anyOfProfile && profile.type === "application") {
+ button = ;
+ } else if (anyOfProfile && profile.type === "setlist") {
+ button = ;
+ }
+
+ return
+
+
setSelectedProfile(undefined)}>
+
+
+
+
+
+
+
+
+
+ Official
+
+ {metadata.name}
+
+
+
+
+
+
+
+
+
+ {profile.type === "application"
+ ? `About ${metadata.name} (${(metadata as ApplicationMetadata).releaseName})`
+ : `About ${metadata.name}`}
+
+
+ {metadata.description}
+
+
+ {profile.type === "setlist" &&
+
+ }
+
+
+
;
+};
+
+export default MarketplacePopup;
diff --git a/src/routes/Marketplace/MarketplaceProfileView.tsx b/src/routes/Marketplace/MarketplaceProfileView.tsx
new file mode 100644
index 0000000..cf5ac79
--- /dev/null
+++ b/src/routes/Marketplace/MarketplaceProfileView.tsx
@@ -0,0 +1,34 @@
+import { MarketplaceProfile } from "@app/profiles/marketplace";
+import styles from "./Marketplace.module.css";
+import ProfileIcon from "@app/components/ProfileIcon";
+import { localizeObject } from "@app/utils/localized";
+import { processAssetUrl } from "@app/profiles/utils";
+
+interface Props {
+ profile: MarketplaceProfile,
+ setSelectedProfile: React.Dispatch>,
+}
+
+const MarketplaceProfileView: React.FC = ({ profile, setSelectedProfile }: Props) => {
+ const localized = localizeObject(profile, "en-US");
+ let bannerUrl = localized.bannerUrl;
+ if (bannerUrl === undefined) {
+ bannerUrl = localized.iconUrl;
+ }
+
+ return ;
+};
+
+export default MarketplaceProfileView;
diff --git a/src/routes/Marketplace/MarketplaceSection.tsx b/src/routes/Marketplace/MarketplaceSection.tsx
new file mode 100644
index 0000000..0c4510f
--- /dev/null
+++ b/src/routes/Marketplace/MarketplaceSection.tsx
@@ -0,0 +1,43 @@
+import Button, { ButtonColor } from "@app/components/Button";
+import styles from "./Marketplace.module.css";
+import { useRef, useState } from "react";
+import { useOverflow } from "use-overflow";
+
+type Props = React.PropsWithChildren<{
+ name: string,
+}>;
+
+const MarketplaceSection: React.FC = ({ name, children }: Props) => {
+ const listRef = useRef(null);
+ const { refXOverflowing: listOverflowing } = useOverflow(listRef);
+
+ const [expanded, setExpanded] = useState(false);
+
+ const classes = [styles.section];
+ if (expanded) {
+ classes.push(styles.expanded);
+ }
+
+ return
+
+
+ {name}
+
+ {(expanded || listOverflowing) &&
+
+ }
+
+
+ {children}
+
+
;
+};
+
+export default MarketplaceSection;
diff --git a/src/routes/Marketplace/index.tsx b/src/routes/Marketplace/index.tsx
new file mode 100644
index 0000000..e31dbdf
--- /dev/null
+++ b/src/routes/Marketplace/index.tsx
@@ -0,0 +1,101 @@
+import styles from "./Marketplace.module.css";
+import MarketplaceSection from "./MarketplaceSection";
+import MarketplaceProfileView from "./MarketplaceProfileView";
+import { processAssetUrl } from "@app/profiles/utils";
+import { localizeObject } from "@app/utils/localized";
+import Button, { ButtonColor } from "@app/components/Button";
+import { MarketplaceProfile } from "@app/profiles/marketplace";
+import { askOpenUrl } from "@app/utils/safeUrl";
+import MarketplacePopup from "./MarketplacePopup";
+import { useEffect, useState } from "react";
+import { showErrorDialog } from "@app/dialogs";
+import useMarketIndex from "@app/hooks/useMarketIndex";
+import { settingsManager } from "@app/settings";
+
+function Marketplace() {
+ const [selectedProfile, setSelectedProfile] = useState(undefined);
+ const marketIndexQuery = useMarketIndex();
+
+ // Used for the "Updated!" tag
+ useEffect(() => {
+ (async () => {
+ await settingsManager.set("lastMarketplaceObserve", new Date().toISOString());
+ })();
+ }, []);
+
+ if (marketIndexQuery.isLoading) {
+ return <>Loading...>;
+ }
+
+ const marketIndex = marketIndexQuery.data;
+ if (marketIndexQuery.isError || marketIndex === undefined) {
+ return <>
+ Error: {marketIndexQuery.error}
+ >;
+ }
+
+ const banner = localizeObject(marketIndex.banner, "en-US");
+
+ return
+
+
+
+
+
+ New!
+
+ {banner.preHeaderText !== undefined &&
+
+ {banner.preHeaderText}
+
+ }
+
+ {banner.headerText}
+
+
+
+
+ {banner.previewUrl !== undefined &&
+
+ }
+
+
+
+
+ {
+ marketIndex.profiles.filter(i => i.type === "application").map(i =>
+
+ )
+ }
+
+
+ {
+ marketIndex.profiles.filter(i => i.type === "setlist").map(i =>
+
+ )
+ }
+
+
+ ;
+}
+
+export default Marketplace;
diff --git a/src/routes/Queue/index.tsx b/src/routes/Queue/index.tsx
index 593bbff..ed472ed 100644
--- a/src/routes/Queue/index.tsx
+++ b/src/routes/Queue/index.tsx
@@ -90,4 +90,4 @@ function Queue() {
>;
}
-export default Queue;
\ No newline at end of file
+export default Queue;
diff --git a/src/routes/Setlist/Official.tsx b/src/routes/Setlist/Official.tsx
deleted file mode 100644
index c31fc1b..0000000
--- a/src/routes/Setlist/Official.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import SetlistPage from "@app/components/Setlist/SetlistPage";
-import { useSetlistData } from "@app/hooks/useSetlistData";
-import { useSetlistRelease } from "@app/hooks/useSetlistRelease";
-
-function OfficialSetlistPage() {
- const { data: setlistData, error, isSuccess, isLoading } = useSetlistRelease("official");
- const setlistVersion = useSetlistData(setlistData, "official");
-
- if (isLoading) return "Loading...";
-
- if (error) return `An error has occurred: ${error}`;
-
- if (isSuccess) {
- return (<>
-
- >);
- }
-}
-
-export default OfficialSetlistPage;
\ No newline at end of file
diff --git a/src/routes/YARG/Nightly.tsx b/src/routes/YARG/Nightly.tsx
deleted file mode 100644
index 877e38e..0000000
--- a/src/routes/YARG/Nightly.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import LaunchPage from "@app/components/Launch/LaunchPage";
-import { useYARGRelease } from "@app/hooks/useYARGRelease";
-import { useYARGVersion } from "@app/hooks/useYARGVersion";
-import NightlyYARGIcon from "@app/assets/NightlyYARGIcon.png";
-import NightlyYARGBanner from "@app/assets/Banner/Nightly.png";
-
-function NightlyYARGPage() {
- const { data: releaseData, error, isSuccess, isLoading } = useYARGRelease("nightly");
- const yargVersion = useYARGVersion(releaseData, "nightly");
-
- if (isLoading) return "Loading...";
-
- if (error) return `An error has occurred: ${error}`;
-
- if (isSuccess) {
- return (<>
-
- YARG Nightly (a.k.a. YARG bleeding-edge) is an alternative version of YARG that is updated twice
- a day (if changes have been made). These builds are in an extremely early beta, so bugs are expected.
- If you do notice a bug, please be sure to report it on GitHub, or on our Discord.
- >}
- websiteUrl="https://github.com/YARC-Official/YARG-BleedingEdge"
- icon={NightlyYARGIcon}
- banner={NightlyYARGBanner}
- />
- >);
- }
-}
-
-export default NightlyYARGPage;
\ No newline at end of file
diff --git a/src/routes/YARG/Stable.tsx b/src/routes/YARG/Stable.tsx
deleted file mode 100644
index fd54d0b..0000000
--- a/src/routes/YARG/Stable.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import LaunchPage from "@app/components/Launch/LaunchPage";
-import { useYARGRelease } from "@app/hooks/useYARGRelease";
-import { useYARGVersion } from "@app/hooks/useYARGVersion";
-import StableYARGIcon from "@app/assets/StableYARGIcon.png";
-import StableYARGBanner from "@app/assets/Banner/Stable.png";
-
-function StableYARGPage() {
- const { data: releaseData, error, isSuccess, isLoading } = useYARGRelease("stable");
- const yargVersion = useYARGVersion(releaseData, "stable");
-
- if (isLoading) return "Loading...";
-
- if (error) return `An error has occurred: ${error}`;
-
- if (isSuccess) {
- return (<>
-
- YARG (a.k.a. Yet Another Rhythm Game) is a free, open-source, plastic guitar game that is
- still in development. It supports guitar (five fret), drums (plastic or e-kit), vocals,
- pro-guitar, and more!
- >}
- websiteUrl="https://github.com/YARC-Official/YARG"
- icon={StableYARGIcon}
- banner={StableYARGBanner}
- />
- >);
- }
-}
-
-export default StableYARGPage;
\ No newline at end of file
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 0e9551e..7d8aa0d 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -3,11 +3,10 @@ import { createBrowserRouter } from "react-router-dom";
import RootLayout from "@app/routes/root";
import Home from "@app/routes/Home";
import Settings from "@app/routes/Settings";
-import StableYARGPage from "./YARG/Stable";
-import NightlyYARGPage from "./YARG/Nightly";
-import OfficialSetlistPage from "./Setlist/Official";
import Queue from "@app/routes/Queue";
import NewsPage from "./NewsPage";
+import AppProfile from "./AppProfile";
+import Marketplace from "./Marketplace";
const Router = createBrowserRouter([
{
@@ -18,32 +17,22 @@ const Router = createBrowserRouter([
path: "/",
element:
},
-
{
path: "/settings",
element:
},
-
{
path: "/queue",
element:
},
-
- {
- path: "/yarg/stable",
- element:
- },
-
{
- path: "/yarg/nightly",
- element:
+ path: "/marketplace",
+ element:
},
-
{
- path: "/setlist/official",
- element:
+ path: "/app-profile/:uuid",
+ element:
},
-
{
path: "/news/:md",
element:
@@ -52,4 +41,4 @@ const Router = createBrowserRouter([
},
]);
-export default Router;
\ No newline at end of file
+export default Router;
diff --git a/src/routes/root.tsx b/src/routes/root.tsx
index d1f363a..52f5fa2 100644
--- a/src/routes/root.tsx
+++ b/src/routes/root.tsx
@@ -4,14 +4,12 @@ import Sidebar from "@app/components/Sidebar";
import { Outlet } from "react-router-dom";
const RootLayout: React.FC = () => {
- return (<>
-
+ return <>
-
- >);
+ >;
};
-export default RootLayout;
\ No newline at end of file
+export default RootLayout;
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..a3a948e
--- /dev/null
+++ b/src/settings.ts
@@ -0,0 +1,18 @@
+import { SettingsManager } from "tauri-settings";
+import { ActiveProfile } from "./profiles/types";
+
+export interface Settings {
+ onboardingCompleted: boolean,
+ downloadLocation: string,
+ lastMarketplaceObserve: string,
+ activeProfiles: ActiveProfile[],
+}
+
+export const settingsManager = new SettingsManager({
+ onboardingCompleted: false,
+ downloadLocation: "",
+ lastMarketplaceObserve: new Date().toISOString(),
+ activeProfiles: []
+}, {
+ prettify: true
+});
diff --git a/src/stores/SetlistStateStore.ts b/src/stores/SetlistStateStore.ts
deleted file mode 100644
index 3017e73..0000000
--- a/src/stores/SetlistStateStore.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { SetlistStates } from "@app/hooks/useSetlistData";
-import { create } from "zustand";
-
-interface SetlistStateStore {
- states: {
- [key: string]: SetlistStates
- },
- update: (key: string, state: SetlistStates) => void
-}
-
-const useSetlistStateStore = create()((set) => ({
- states: {},
- update(key, state) {
- return set(current => ({
- states: {
- ...current.states,
- [key]: state
- }
- }));
- },
-}));
-
-interface useSetlistStateInterface {
- state: SetlistStates;
- setState: (newState: SetlistStates) => void;
-}
-
-export const useSetlistState = (version?: string): useSetlistStateInterface => {
- const store = useSetlistStateStore();
-
- // If we don't have a version yet, return a dummy loading version;
- if (!version) {
- return {
- state: SetlistStates.LOADING,
- setState: () => {}
- };
- }
-
- const state = store.states[version];
- const setState = (newState: SetlistStates) => store.update(version, newState);
-
- return { state, setState };
-};
\ No newline at end of file
diff --git a/src/stores/YARGStateStore.ts b/src/stores/YARGStateStore.ts
deleted file mode 100644
index df9a9bd..0000000
--- a/src/stores/YARGStateStore.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { YARGStates } from "@app/hooks/useYARGVersion";
-import { create } from "zustand";
-
-interface YARGStateStore {
- states: {
- [key: string]: YARGStates
- },
- update: (key: string, state: YARGStates) => void
-}
-
-const useYARGStateStore = create()((set) => ({
- states: {},
- update(key, state) {
- return set(current => ({
- states: {
- ...current.states,
- [key]: state
- }
- }));
- },
-}));
-
-interface useYARGStateInterface {
- state: YARGStates;
- setState: (newState: YARGStates) => void;
-}
-
-export const useYARGState = (version?: string): useYARGStateInterface => {
- const store = useYARGStateStore();
-
- // If we don't have a version yet, return a dummy loading version;
- if (!version) {
- return {
- state: YARGStates.LOADING,
- setState: () => {}
- };
- }
-
- const state = store.states[version];
- const setState = (newState: YARGStates) => store.update(version, newState);
-
- return { state, setState };
-};
\ No newline at end of file
diff --git a/src/styles.css b/src/styles.css
index 688253a..4a08ca6 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1,4 +1,7 @@
:root {
+ --backupFonts: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji";
+
--accent-rgb: 46, 217, 255;
--accent: rgb(var(--accent-rgb));
@@ -9,38 +12,32 @@
--primary-40: rgba(var(--primary-rgb), 0.4);
--primary-75: rgba(var(--primary-rgb), 0.75);
- --white-background: #F5F5F5;
-
- --green-rgb: 70, 231, 79;
- --green: rgb(var(--green-rgb));
- --green-10: rgba(var(--green-rgb), .1);
+ --titleBarHeight: 35px;
- --titleBar_background: #000209;
- --titleBar_primary: #FFF;
- --titleBar_accent: #868AA8;
- --titleBar_height: 30px;
+ --sideBarBackground: #070810;
+ --sideBarWidth: 300px;
- --sideBar_background: #05060B;
- --sideBar_separator_color: #394452;
- --sideBar_selection: rgba(255, 255, 255, 0.1);
+ --buttonGreen: #17E289;
+ --buttonBlue: #2ED9FF;
+ --buttonYellow: #FCD548;
+ --buttonDark: #0F121D;
+ --buttonLight: #F4F4F4;
+ --buttonRed: #F32B37;
- --button_green: #46E74F;
- --button_blue: #2ED9FF;
- --button_yellow: #FFB800;
- --button_gray: #E6E6E6;
- --button_red: #F32B37;
+ --buttonGreenText: #005832;
+ --buttonBlueText: #03596B;
+ --buttonYellowText: #4F2600;
+ --buttonDarkText: #E0E1E7;
+ --buttonLightText: #41475F;
+ --buttonRedText: #FFF;
- --buttonText_green: #003C03;
- --buttonText_blue: #002A3D;
- --buttonText_yellow: #351A00;
- --buttonText_gray: #737373;
- --buttonText_red: #FFF;
+ --buttonLightBorder: rgba(255, 255, 255, 0.4);
+ --buttonDarkBorder: #222638;
}
* {
box-sizing: border-box;
- font-family: "Inter", -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial,
- sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
+ font-family: "Inter", var(--backupFonts);
}
body {
@@ -54,35 +51,11 @@ a {
text-decoration: none;
}
-.titlebar {
- height: 30px;
- background: #329ea3;
- user-select: none;
- display: flex;
- justify-content: flex-end;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
-}
-
-.titlebar-button {
- display: inline-flex;
- justify-content: center;
- align-items: center;
- width: 30px;
- height: 30px;
-}
-
-.titlebar-button:hover {
- background: #5bbec3;
-}
-
#root {
width: 100%;
height: 100%;
display: flex;
- padding-top: var(--titleBar_height);
+ padding-top: var(--titleBarHeight);
}
#content {
@@ -93,6 +66,7 @@ a {
::-webkit-scrollbar {
width: 10px;
+ height: 10px;
}
::-webkit-scrollbar-track {
@@ -101,8 +75,9 @@ a {
::-webkit-scrollbar-thumb {
background: #888;
+ border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #666;
-}
\ No newline at end of file
+}
diff --git a/src/tasks/Processors/DownloadAndInstallTask.tsx b/src/tasks/Processors/DownloadAndInstallTask.tsx
new file mode 100644
index 0000000..c429f7e
--- /dev/null
+++ b/src/tasks/Processors/DownloadAndInstallTask.tsx
@@ -0,0 +1,58 @@
+import { ActiveProfile } from "@app/profiles/types";
+import { BaseTask, IBaseTask } from "./base";
+import { invoke } from "@tauri-apps/api";
+import { ReactNode } from "react";
+import QueueEntry from "@app/components/Queue/QueueEntry";
+import { localizeObject } from "@app/utils/localized";
+import { showErrorDialog } from "@app/dialogs";
+import ProfileIcon from "@app/components/ProfileIcon";
+
+export class DownloadAndInstallTask extends BaseTask implements IBaseTask {
+ onFinish?: () => void;
+
+ tempPath: string;
+
+ constructor(profile: ActiveProfile, profilePath: string, tempPath: string, onFinish?: () => void) {
+ super(profile, profilePath);
+
+ this.onFinish = onFinish;
+
+ this.tempPath = tempPath;
+ }
+
+ async start(): Promise {
+ try {
+ await invoke("download_and_install_profile", {
+ profilePath: this.profilePath,
+ uuid: this.activeProfile.uuid,
+ tag: this.activeProfile.version.tag,
+ tempPath: this.tempPath,
+ content: this.activeProfile.version.content
+ });
+ } catch (e) {
+ showErrorDialog(e as string);
+ }
+ }
+
+ getQueueEntry(bannerMode: boolean): ReactNode {
+ const profile = this.activeProfile.profile;
+ if (profile.type === "application") {
+ const metadata = localizeObject(profile.metadata, "en-US");
+
+ return }
+ bannerMode={bannerMode} />;
+ } else {
+ const metadata = localizeObject(profile.metadata, "en-US");
+
+ return }
+ bannerMode={bannerMode} />;
+ }
+ }
+}
diff --git a/src/tasks/Processors/Setlist.tsx b/src/tasks/Processors/Setlist.tsx
deleted file mode 100644
index b3cf974..0000000
--- a/src/tasks/Processors/Setlist.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { invoke } from "@tauri-apps/api/tauri";
-import { BaseTask, IBaseTask } from "./base";
-import SetlistQueue from "@app/components/Queue/QueueEntry/Setlist";
-
-export abstract class SetlistTask extends BaseTask {
- profile: string;
- version: string;
- onFinish: () => void;
-
- constructor(profile: string, version: string, onFinish: () => void) {
- super("setlist", profile);
-
- this.profile = profile;
- this.version = version;
- this.onFinish = onFinish;
- }
-
- getQueueEntry(bannerMode: boolean): React.ReactNode {
- return ;
- }
-}
-
-export class SetlistDownload extends SetlistTask implements IBaseTask {
- zipUrls: string[];
-
- constructor(zipUrls: string[], profile: string, version: string, onFinish: () => void) {
- super(profile, version, onFinish);
-
- this.zipUrls = zipUrls;
- }
-
- async start(): Promise {
- return await invoke("download_and_install", {
- appName: "official_setlist",
- version: this.version,
- profile: this.profile,
- zipUrls: this.zipUrls,
- sigUrls: [],
- });
- }
-}
-
-export class SetlistUninstall extends SetlistTask implements IBaseTask {
- constructor(profile: string, version: string, onFinish: () => void) {
- super(profile, version, onFinish);
- }
-
- async start(): Promise {
- return await invoke("uninstall", {
- appName: "official_setlist",
- version: this.version,
- profile: this.profile
- });
- }
-}
\ No newline at end of file
diff --git a/src/tasks/Processors/UninstallTask.tsx b/src/tasks/Processors/UninstallTask.tsx
new file mode 100644
index 0000000..2c35d1c
--- /dev/null
+++ b/src/tasks/Processors/UninstallTask.tsx
@@ -0,0 +1,47 @@
+import { ActiveProfile } from "@app/profiles/types";
+import { BaseTask, IBaseTask } from "./base";
+import { invoke } from "@tauri-apps/api";
+import { ReactNode } from "react";
+import QueueEntry from "@app/components/Queue/QueueEntry";
+import { localizeObject } from "@app/utils/localized";
+import { showErrorDialog } from "@app/dialogs";
+import ProfileIcon from "@app/components/ProfileIcon";
+
+export class UninstallTask extends BaseTask implements IBaseTask {
+ onFinish?: () => void;
+
+ constructor(profile: ActiveProfile, profilePath: string, onFinish?: () => void) {
+ super(profile, profilePath);
+ this.onFinish = onFinish;
+ }
+
+ async start(): Promise {
+ try {
+ await invoke("uninstall_profile", {
+ profilePath: this.profilePath
+ });
+ } catch (e) {
+ showErrorDialog(e as string);
+ }
+ }
+
+ getQueueEntry(bannerMode: boolean): ReactNode {
+ const profile = this.activeProfile.profile;
+ if (profile.type === "application") {
+ const metadata = localizeObject(profile.metadata, "en-US");
+
+ return }
+ bannerMode={bannerMode} />;
+ } else {
+ const metadata = localizeObject(profile.metadata, "en-US");
+
+ return }
+ bannerMode={bannerMode} />;
+ }
+ }
+}
diff --git a/src/tasks/Processors/YARG.tsx b/src/tasks/Processors/YARG.tsx
deleted file mode 100644
index 455db5a..0000000
--- a/src/tasks/Processors/YARG.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { invoke } from "@tauri-apps/api/tauri";
-import { BaseTask, IBaseTask } from "./base";
-import YARGQueue from "@app/components/Queue/QueueEntry/YARG";
-import { YARGChannels } from "@app/hooks/useYARGRelease";
-
-export abstract class YARGTask extends BaseTask {
- channel: YARGChannels;
- version: string;
- profile: string;
- onFinish: () => void;
-
- constructor(channel: YARGChannels, version: string, profile: string, onFinish: () => void) {
- super("yarg", profile);
-
- this.channel = channel;
- this.version = version;
- this.profile = profile;
- this.onFinish = onFinish;
- }
-
- getQueueEntry(bannerMode: boolean): React.ReactNode {
- return ;
- }
-}
-
-export class YARGDownload extends YARGTask implements IBaseTask {
- zipUrl: string;
- sigUrl?: string;
-
- constructor(zipUrl: string, sigUrl: string | undefined, channel: YARGChannels, version: string,
- profile: string, onFinish: () => void) {
-
- super(channel, version, profile, onFinish);
-
- this.zipUrl = zipUrl;
- this.sigUrl = sigUrl;
- }
-
- async start(): Promise {
- let sigUrls: string[] = [];
- if (this.sigUrl != null) {
- sigUrls = [ this.sigUrl ];
- }
-
- return await invoke("download_and_install", {
- appName: "yarg",
- version: this.version,
- profile: this.profile,
- zipUrls: [ this.zipUrl ],
- sigUrls: sigUrls,
- });
- }
-}
-
-export class YARGUninstall extends YARGTask implements IBaseTask {
- constructor(channel: YARGChannels, version: string, profile: string, onFinish: () => void) {
- super(channel, version, profile, onFinish);
- }
-
- async start(): Promise {
- return await invoke("uninstall", {
- appName: "yarg",
- version: this.version,
- profile: this.profile
- });
- }
-}
\ No newline at end of file
diff --git a/src/tasks/Processors/base.ts b/src/tasks/Processors/base.ts
index a5cdb0c..6df10bd 100644
--- a/src/tasks/Processors/base.ts
+++ b/src/tasks/Processors/base.ts
@@ -1,12 +1,12 @@
+import { ActiveProfile } from "@app/profiles/types";
import { v4 as generateUUID } from "uuid";
-export type TaskTag = "yarg" | "setlist";
-
export interface IBaseTask {
- startedAt?: Date,
- taskUUID: string,
- taskTag: TaskTag,
- profile: string,
+ startedAt?: Date;
+ taskUUID: string;
+
+ activeProfile: ActiveProfile;
+ profilePath: string;
onFinish?: () => void;
@@ -15,13 +15,16 @@ export interface IBaseTask {
}
export class BaseTask {
+ startedAt?: Date;
taskUUID: string;
- taskTag: TaskTag;
- profile: string;
- constructor(taskTag: TaskTag, profile: string) {
+ activeProfile: ActiveProfile;
+ profilePath: string;
+
+ constructor(profile: ActiveProfile, profilePath: string) {
this.taskUUID = generateUUID();
- this.taskTag = taskTag;
- this.profile = profile;
+
+ this.activeProfile = profile;
+ this.profilePath = profilePath;
}
-}
\ No newline at end of file
+}
diff --git a/src/tasks/index.ts b/src/tasks/index.ts
index 540030a..f594b90 100644
--- a/src/tasks/index.ts
+++ b/src/tasks/index.ts
@@ -1,19 +1,21 @@
import { useStore } from "zustand";
-import { IBaseTask, TaskTag } from "./Processors/base";
+import { IBaseTask } from "./Processors/base";
import QueueStore from "./queue";
-import { showErrorDialog } from "@app/dialogs/dialogUtil";
+import { showErrorDialog } from "@app/dialogs";
const addTask = (task: IBaseTask) => {
QueueStore.add(task);
- if(QueueStore.firstTask() === task) {
+ if (QueueStore.firstTask() === task) {
processNextTask();
}
};
const processNextTask = async () => {
const next = QueueStore.next();
- if(!next) return;
+ if (!next) {
+ return;
+ }
try {
next.startedAt = new Date();
@@ -27,10 +29,10 @@ const processNextTask = async () => {
processNextTask();
};
-const useTask = (tag: TaskTag, profile: string) => {
+const useTask = (profileUUID: string) => {
return useStore(
QueueStore.store,
- queue => QueueStore.findTask(queue, tag, profile)
+ queue => QueueStore.findTask(queue, profileUUID)
);
};
@@ -41,4 +43,4 @@ const useCurrentTask = () => {
);
};
-export { addTask, processNextTask, useTask, useCurrentTask };
\ No newline at end of file
+export { addTask, processNextTask, useTask, useCurrentTask };
diff --git a/src/tasks/queue.ts b/src/tasks/queue.ts
index aac004a..1571783 100644
--- a/src/tasks/queue.ts
+++ b/src/tasks/queue.ts
@@ -1,5 +1,5 @@
import { createStore } from "zustand/vanilla";
-import { IBaseTask, TaskTag } from "./Processors/base";
+import { IBaseTask } from "./Processors/base";
import { useStore } from "zustand";
type TaskQueueStore = Set;
@@ -26,16 +26,18 @@ const remove = (task: IBaseTask) => {
const next = () => {
const current = firstTask();
- if(current?.startedAt) {
+ if (current?.startedAt) {
remove(current);
}
return firstTask();
};
-const findTask = (queue: TaskQueueStore, tag: TaskTag, profile: string) => {
- for(const task of queue) {
- if(task.taskTag === tag && task.profile === profile) return task;
+const findTask = (queue: TaskQueueStore, profileUUID: string) => {
+ for (const task of queue) {
+ if (task.activeProfile.uuid === profileUUID) {
+ return task;
+ }
}
return undefined;
@@ -46,4 +48,4 @@ const useQueue = () => {
};
export const QueueStore = { store, firstTask, add, remove, next, findTask, useQueue };
-export default QueueStore;
\ No newline at end of file
+export default QueueStore;
diff --git a/src/utils/localized.ts b/src/utils/localized.ts
new file mode 100644
index 0000000..096e84f
--- /dev/null
+++ b/src/utils/localized.ts
@@ -0,0 +1,41 @@
+export type Localized = T & {
+ localeOverrides: {
+ [locales: string]: Partial
+ }
+}
+
+export const localize = (obj: Localized, key: K, locale: string): T[K] => {
+ let value: T[K] = obj[key];
+
+ if (locale in obj.localeOverrides) {
+ const newValue = obj.localeOverrides[locale][key];
+ if (newValue !== undefined) {
+ value = newValue as T[K];
+ }
+ }
+
+ return value;
+};
+
+export const localizeObject = (obj: Localized, locale: string): T => {
+ const {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ localeOverrides,
+ ...omittedObj
+ } = obj;
+ const newObj = omittedObj as T;
+
+ if (locale in obj.localeOverrides) {
+ const override = obj.localeOverrides[locale];
+
+ let key: keyof T;
+ for (key in override) {
+ const value = override[key];
+ if (value !== undefined) {
+ newObj[key] = value as T[keyof T];
+ }
+ }
+ }
+
+ return newObj;
+};
diff --git a/src/utils/os.ts b/src/utils/os.ts
new file mode 100644
index 0000000..38c6bb5
--- /dev/null
+++ b/src/utils/os.ts
@@ -0,0 +1,11 @@
+import { type } from "@tauri-apps/api/os";
+
+export type OS = "windows" | "macos" | "linux";
+
+export const getOS = async (): Promise => {
+ switch (await type()) {
+ case "Windows_NT": return "windows";
+ case "Darwin": return "macos";
+ case "Linux": return "linux";
+ }
+};
diff --git a/src/utils/safeUrl.ts b/src/utils/safeUrl.ts
new file mode 100644
index 0000000..f7549e9
--- /dev/null
+++ b/src/utils/safeUrl.ts
@@ -0,0 +1,11 @@
+import { createAndShowDialog } from "@app/dialogs";
+import { LeavingLauncherDialog } from "@app/dialogs/Dialogs/LeavingLauncherDialog";
+import { open } from "@tauri-apps/api/shell";
+
+export async function askOpenUrl(url: string) {
+ await createAndShowDialog(LeavingLauncherDialog, { url });
+}
+
+export function openUrl(url: string) {
+ open(url);
+}
diff --git a/src/utils/timeFormat.ts b/src/utils/timeFormat.ts
index ef40475..f1e6770 100644
--- a/src/utils/timeFormat.ts
+++ b/src/utils/timeFormat.ts
@@ -1,7 +1,13 @@
+import { intlFormatDistance } from "date-fns";
+
export const millisToDisplayLength = (length: number, long = false) => {
const date = new Date(length);
if (long) {
- return `${date.getMinutes()} min ${date.getSeconds()} sec`;
+ if (date.getHours() !== 0) {
+ return `${date.getHours()} hr ${date.getMinutes()} min ${date.getSeconds()} sec`;
+ } else {
+ return `${date.getMinutes()} min ${date.getSeconds()} sec`;
+ }
} else {
return new Intl.DateTimeFormat("en-US", {
minute: "numeric",
@@ -25,4 +31,8 @@ export const isConsideredNewRelease = (releaseDate: string, newestInSetlist: str
}
return true;
-};
\ No newline at end of file
+};
+
+export const distanceFromToday = (initial: string) => {
+ return intlFormatDistance(new Date(initial), new Date());
+};