mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-13 16:22:43 +10:00
Move frontend to main folder (#109)
* feat: small refactor * fix: appimage build script * fix: add NO_STRIP to AppImage build * fix: build and dev mode from refactor * fix: submodule step 1 * fix: submodules step 2
This commit is contained in:
31
main/components/GameOptions/Launch.vue
Normal file
31
main/components/GameOptions/Launch.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div>
|
||||
<label for="launch" class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Launch string template</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
type="text"
|
||||
name="launch"
|
||||
id="launch"
|
||||
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-800 placeholder:text-zinc-400 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
placeholder="{}"
|
||||
aria-describedby="launch-description"
|
||||
v-model="model!!.launchString"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-400" id="launch-description">
|
||||
Override the launch string. Passed to system's default shell, and replaces
|
||||
"{}" with the command to start the game.
|
||||
<span class="font-semibold text-zinc-200"
|
||||
>Leaving it blank will cause the game not to start.</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FrontendGameConfiguration } from "~/composables/game";
|
||||
|
||||
const model = defineModel<FrontendGameConfiguration>();
|
||||
</script>
|
||||
122
main/components/GameOptionsModal.vue
Normal file
122
main/components/GameOptionsModal.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<ModalTemplate size-class="max-w-4xl" v-model="open">
|
||||
<template #default>
|
||||
<div class="flex flex-row gap-x-4">
|
||||
<nav class="flex flex-1 flex-col" aria-label="Sidebar">
|
||||
<ul role="list" class="-mx-2 space-y-1">
|
||||
<li v-for="(tab, tabIdx) in tabs" :key="tab.name">
|
||||
<button
|
||||
@click="() => (currentTabIndex = tabIdx)"
|
||||
:class="[
|
||||
tabIdx == currentTabIndex
|
||||
? 'bg-zinc-800 text-zinc-100'
|
||||
: 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100',
|
||||
'transition w-full group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="tab.icon"
|
||||
:class="[
|
||||
tabIdx == currentTabIndex
|
||||
? 'text-zinc-100'
|
||||
: 'text-gray-400 group-hover:text-zinc-100',
|
||||
'size-6 shrink-0',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="border-l-2 border-zinc-800 w-full grow pl-4">
|
||||
<component
|
||||
v-model="configuration"
|
||||
:is="tabs[currentTabIndex]?.page"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="saveError" class="mt-5 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ saveError }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<LoadingButton
|
||||
@click="() => save()"
|
||||
:loading="saveLoading"
|
||||
type="submit"
|
||||
class="ml-2 w-full sm:w-fit"
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
<button
|
||||
@click="() => (open = false)"
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
|
||||
ref="cancelButtonRef"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
import {
|
||||
RocketLaunchIcon,
|
||||
ServerIcon,
|
||||
TrashIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import Launch from "./GameOptions/Launch.vue";
|
||||
import type { FrontendGameConfiguration } from "~/composables/game";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const open = defineModel<boolean>();
|
||||
const props = defineProps<{ gameId: string }>();
|
||||
const game = await useGame(props.gameId);
|
||||
|
||||
const configuration: Ref<FrontendGameConfiguration> = ref({
|
||||
launchString: game.version!!.launchCommandTemplate,
|
||||
});
|
||||
|
||||
const tabs: Array<{ name: string; icon: Component; page: Component }> = [
|
||||
{
|
||||
name: "Launch",
|
||||
icon: RocketLaunchIcon,
|
||||
page: Launch,
|
||||
},
|
||||
{
|
||||
name: "Storage",
|
||||
icon: ServerIcon,
|
||||
page: h("div"),
|
||||
},
|
||||
];
|
||||
const currentTabIndex = ref(0);
|
||||
|
||||
const saveLoading = ref(false);
|
||||
const saveError = ref<undefined | string>();
|
||||
async function save() {
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
await invoke("update_game_configuration", {
|
||||
gameId: game.game.id,
|
||||
options: configuration.value,
|
||||
});
|
||||
open.value = false;
|
||||
} catch (e) {
|
||||
saveError.value = (e as unknown as string).toString();
|
||||
}
|
||||
saveLoading.value = false;
|
||||
}
|
||||
</script>
|
||||
183
main/components/GameStatusButton.vue
Normal file
183
main/components/GameStatusButton.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||
<div class="inline-flex divide-x divide-zinc-900">
|
||||
<button
|
||||
type="button"
|
||||
@click="() => buttonActions[props.status.type]()"
|
||||
:class="[
|
||||
styles[props.status.type],
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="buttonIcons[props.status.type]"
|
||||
class="-mr-0.5 size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
<Menu
|
||||
v-if="showDropdown"
|
||||
as="div"
|
||||
class="relative inline-block text-left grow"
|
||||
>
|
||||
<div class="h-full">
|
||||
<MenuButton
|
||||
:class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
|
||||
>
|
||||
<div class="py-1">
|
||||
<MenuItem v-if="showOptions" v-slot="{ active }">
|
||||
<button
|
||||
@click="() => emit('options')"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
Options
|
||||
<Cog6ToothIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button
|
||||
@click="() => emit('uninstall')"
|
||||
:class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ChevronDownIcon,
|
||||
PlayIcon,
|
||||
QueueListIcon,
|
||||
ServerIcon,
|
||||
StopIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
|
||||
import type { Component } from "vue";
|
||||
import { GameStatusEnum, type GameStatus } from "~/types.js";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||
import { Cog6ToothIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const props = defineProps<{ status: GameStatus }>();
|
||||
const emit = defineEmits<{
|
||||
(e: "install"): void;
|
||||
(e: "launch"): void;
|
||||
(e: "queue"): void;
|
||||
(e: "uninstall"): void;
|
||||
(e: "kill"): void;
|
||||
(e: "options"): void;
|
||||
(e: "resume"): void;
|
||||
}>();
|
||||
|
||||
const showDropdown = computed(
|
||||
() =>
|
||||
props.status.type === GameStatusEnum.Installed ||
|
||||
props.status.type === GameStatusEnum.SetupRequired ||
|
||||
props.status.type === GameStatusEnum.PartiallyInstalled
|
||||
);
|
||||
|
||||
const showOptions = computed(
|
||||
() => props.status.type === GameStatusEnum.Installed
|
||||
);
|
||||
|
||||
const styles: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]:
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||
[GameStatusEnum.Queued]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Downloading]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Validating]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.SetupRequired]:
|
||||
"bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500",
|
||||
[GameStatusEnum.Installed]:
|
||||
"bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600 hover:bg-green-500",
|
||||
[GameStatusEnum.Updating]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Uninstalling]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Running]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.PartiallyInstalled]:
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500",
|
||||
};
|
||||
|
||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Install",
|
||||
[GameStatusEnum.Queued]: "Queued",
|
||||
[GameStatusEnum.Downloading]: "Downloading",
|
||||
[GameStatusEnum.Validating]: "Validating",
|
||||
[GameStatusEnum.SetupRequired]: "Setup",
|
||||
[GameStatusEnum.Installed]: "Play",
|
||||
[GameStatusEnum.Updating]: "Updating",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||
[GameStatusEnum.Running]: "Stop",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Resume",
|
||||
};
|
||||
|
||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
[GameStatusEnum.Remote]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Queued]: QueueListIcon,
|
||||
[GameStatusEnum.Downloading]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Validating]: ServerIcon,
|
||||
[GameStatusEnum.SetupRequired]: WrenchIcon,
|
||||
[GameStatusEnum.Installed]: PlayIcon,
|
||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||
[GameStatusEnum.Running]: StopIcon,
|
||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon,
|
||||
};
|
||||
|
||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||
[GameStatusEnum.Remote]: () => emit("install"),
|
||||
[GameStatusEnum.Queued]: () => emit("queue"),
|
||||
[GameStatusEnum.Downloading]: () => emit("queue"),
|
||||
[GameStatusEnum.Validating]: () => emit("queue"),
|
||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||
[GameStatusEnum.Uninstalling]: () => {},
|
||||
[GameStatusEnum.Running]: () => emit("kill"),
|
||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume"),
|
||||
};
|
||||
</script>
|
||||
96
main/components/Header.vue
Normal file
96
main/components/Header.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="h-16 bg-zinc-950 flex flex-row justify-between">
|
||||
<div class="flex flex-row grow items-center pl-5 pr-2 py-3">
|
||||
<div class="inline-flex items-center gap-x-10">
|
||||
<NuxtLink to="/store">
|
||||
<Wordmark class="h-8 mb-0.5" />
|
||||
</NuxtLink>
|
||||
<nav class="inline-flex items-center mt-0.5">
|
||||
<ol class="inline-flex items-center gap-x-6">
|
||||
<NuxtLink
|
||||
v-for="(nav, navIdx) in navigation"
|
||||
:class="[
|
||||
'transition uppercase font-display font-semibold text-md',
|
||||
navIdx === currentNavigation
|
||||
? 'text-zinc-100'
|
||||
: 'text-zinc-400 hover:text-zinc-200',
|
||||
]"
|
||||
:href="nav.route"
|
||||
>
|
||||
{{ nav.label }}
|
||||
</NuxtLink>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div
|
||||
@mousedown="() => window.startDragging()"
|
||||
class="flex cursor-pointer grow h-full"
|
||||
/>
|
||||
<div class="inline-flex items-center">
|
||||
<ol class="inline-flex gap-3">
|
||||
<HeaderQueueWidget :object="currentQueueObject" />
|
||||
<li v-for="(item, itemIdx) in quickActions">
|
||||
<HeaderWidget
|
||||
@click="item.action"
|
||||
:notifications="item.notifications"
|
||||
>
|
||||
<component class="h-5" :is="item.icon" />
|
||||
</HeaderWidget>
|
||||
</li>
|
||||
<OfflineHeaderWidget v-if="state?.status === AppStatus.Offline" />
|
||||
<HeaderUserWidget />
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<WindowControl />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BellIcon, UserGroupIcon } from "@heroicons/vue/16/solid";
|
||||
import { AppStatus, type NavigationItem, type QuickActionNav } from "../types";
|
||||
import HeaderWidget from "./HeaderWidget.vue";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
const window = getCurrentWindow();
|
||||
const state = useAppState();
|
||||
|
||||
const navigation: Array<NavigationItem> = [
|
||||
{
|
||||
prefix: "/store",
|
||||
route: "/store",
|
||||
label: "Store",
|
||||
},
|
||||
{
|
||||
prefix: "/library",
|
||||
route: "/library",
|
||||
label: "Library",
|
||||
},
|
||||
{
|
||||
prefix: "/community",
|
||||
route: "/community",
|
||||
label: "Community",
|
||||
},
|
||||
{
|
||||
prefix: "/news",
|
||||
route: "/news",
|
||||
label: "News",
|
||||
},
|
||||
];
|
||||
|
||||
const { currentNavigation } = useCurrentNavigationIndex(navigation);
|
||||
|
||||
const quickActions: Array<QuickActionNav> = [
|
||||
{
|
||||
icon: UserGroupIcon,
|
||||
action: async () => {},
|
||||
},
|
||||
{
|
||||
icon: BellIcon,
|
||||
action: async () => {},
|
||||
},
|
||||
];
|
||||
|
||||
const queue = useQueueState();
|
||||
const currentQueueObject = computed(() => queue.value.queue.at(0));
|
||||
</script>
|
||||
5
main/components/HeaderButton.vue
Normal file
5
main/components/HeaderButton.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<button class="transition h-full aspect-square text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-[1.1rem]">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
26
main/components/HeaderQueueWidget.vue
Normal file
26
main/components/HeaderQueueWidget.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowDownTrayIcon } from "@heroicons/vue/20/solid";
|
||||
|
||||
const props = defineProps<{ object?: QueueState["queue"][0] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
to="/queue"
|
||||
class="transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 bg-zinc-900 hover:bg-zinc-800 relative"
|
||||
>
|
||||
<ArrowDownTrayIcon
|
||||
:class="[
|
||||
'h-5 z-50',
|
||||
props.object
|
||||
? 'text-white hover:text-zinc-300'
|
||||
: 'text-zinc-600 hover:text-zinc-300',
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
v-if="props.object?.progress"
|
||||
class="transition-all absolute left-0 top-0 bottom-0 bg-blue-600 z-10"
|
||||
:style="{ width: `${props.object.progress * 99 + 1}%` }"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
113
main/components/HeaderUserWidget.vue
Normal file
113
main/components/HeaderUserWidget.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<Menu v-if="state?.user" as="div" class="relative inline-block">
|
||||
<MenuButton>
|
||||
<HeaderWidget>
|
||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||
<img :src="profilePictureUrl" class="w-5 h-5 rounded-sm" />
|
||||
<span class="ml-2 text-sm font-bold">{{
|
||||
state.user.displayName
|
||||
}}</span>
|
||||
<ChevronDownIcon class="ml-3 h-4" />
|
||||
</div>
|
||||
</HeaderWidget>
|
||||
</MenuButton>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
|
||||
>
|
||||
<div class="flex-col gap-y-2">
|
||||
<NuxtLink
|
||||
to="/id/me"
|
||||
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
|
||||
>
|
||||
<div class="inline-flex items-center text-zinc-300">
|
||||
<img :src="profilePictureUrl" class="w-5 h-5 rounded-sm" />
|
||||
<span class="ml-2 text-sm font-bold">{{
|
||||
state.user.displayName
|
||||
}}</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
||||
<div class="flex flex-col mb-1">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<a
|
||||
:href="adminUrl"
|
||||
target="_blank"
|
||||
:class="[
|
||||
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
|
||||
'transition block px-4 py-2 text-sm',
|
||||
]"
|
||||
>
|
||||
Admin Dashboard
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-for="(nav, navIdx) in navigation"
|
||||
v-slot="{ active, close }"
|
||||
>
|
||||
<button
|
||||
@click="() => navigate(close, nav)"
|
||||
:href="nav.route"
|
||||
:class="[
|
||||
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400',
|
||||
'transition text-left block px-4 py-2 text-sm',
|
||||
]"
|
||||
>
|
||||
{{ nav.label }}
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||
import { ChevronDownIcon } from "@heroicons/vue/16/solid";
|
||||
import type { NavigationItem } from "../types";
|
||||
import HeaderWidget from "./HeaderWidget.vue";
|
||||
import { useAppState } from "~/composables/app-state";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const open = ref(false);
|
||||
const router = useRouter();
|
||||
router.afterEach(() => {
|
||||
open.value = false;
|
||||
});
|
||||
|
||||
const state = useAppState();
|
||||
const profilePictureUrl: string = await useObject(
|
||||
state.value?.user?.profilePictureObjectId ?? ""
|
||||
);
|
||||
const adminUrl: string = await invoke("gen_drop_url", {
|
||||
path: "/admin",
|
||||
});
|
||||
|
||||
function navigate(close: () => any, to: NavigationItem) {
|
||||
close();
|
||||
router.push(to.route);
|
||||
}
|
||||
|
||||
const navigation: NavigationItem[] = [
|
||||
{
|
||||
label: "App settings",
|
||||
route: "/settings",
|
||||
prefix: "",
|
||||
},
|
||||
{
|
||||
label: "Quit Drop",
|
||||
route: "/quit",
|
||||
prefix: "",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
17
main/components/HeaderWidget.vue
Normal file
17
main/components/HeaderWidget.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="transition inline-flex items-center cursor-pointer rounded-sm px-4 py-1.5 bg-zinc-900 text-zinc-600 hover:bg-zinc-800 hover:text-zinc-300 relative">
|
||||
<slot />
|
||||
<div v-if="props.notifications !== undefined"
|
||||
class="text-zinc-900 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-xs bg-blue-300 rounded-full w-3.5 h-3.5 text-center">
|
||||
{{ props.notifications }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
notifications?: number
|
||||
}>();
|
||||
|
||||
|
||||
</script>
|
||||
171
main/components/InitiateAuthModule.vue
Normal file
171
main/components/InitiateAuthModule.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div
|
||||
class="grid min-h-full grid-cols-1 grid-rows-[1fr,auto,1fr] lg:grid-cols-[max(50%,36rem),1fr]"
|
||||
>
|
||||
<header
|
||||
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
|
||||
>
|
||||
<Logo class="h-10 w-auto sm:h-12" />
|
||||
</header>
|
||||
<main
|
||||
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
|
||||
>
|
||||
<div class="max-w-lg">
|
||||
<slot />
|
||||
<div class="mt-10">
|
||||
<div>
|
||||
<div v-if="loading" role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span class="inline-flex gap-x-8 items-center" v-else>
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
|
||||
>
|
||||
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
|
||||
</button>
|
||||
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
|
||||
Use a code →
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5" v-if="offerManual">
|
||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||
<p class="mt-1 text-zinc-400 text-sm">
|
||||
You can manually enter the token from your web browser.
|
||||
</p>
|
||||
<div class="inline-flex gap-x-1 mt-2 w-full">
|
||||
<input
|
||||
id="token"
|
||||
name="token"
|
||||
type="text"
|
||||
autocomplete="token"
|
||||
required
|
||||
class="grow block w-full rounded-md border-0 py-1.5 px-3 shadow-sm bg-zinc-950/20 text-zinc-300 ring-1 ring-inset ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
v-model="manualToken"
|
||||
/>
|
||||
<LoadingButton
|
||||
:loading="manualLoading"
|
||||
@click="() => continueManual_wrapper()"
|
||||
class="w-fit"
|
||||
>
|
||||
Submit
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-5 rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ error }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="self-end lg:col-span-2 lg:col-start-1 lg:row-start-3">
|
||||
<div class="border-t border-blue-600 bg-zinc-900 py-10">
|
||||
<nav
|
||||
class="mx-auto flex w-full max-w-7xl items-center gap-x-4 px-6 text-sm leading-7 text-zinc-400 lg:px-8"
|
||||
>
|
||||
<a href="#">Documentation</a>
|
||||
<svg
|
||||
viewBox="0 0 2 2"
|
||||
aria-hidden="true"
|
||||
class="h-0.5 w-0.5 fill-zinc-700"
|
||||
>
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</svg>
|
||||
<a href="#">Troubleshooting</a>
|
||||
<svg
|
||||
viewBox="0 0 2 2"
|
||||
aria-hidden="true"
|
||||
class="h-0.5 w-0.5 fill-zinc-700"
|
||||
>
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</svg>
|
||||
<NuxtLink to="/setup/server">Switch instance</NuxtLink>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
<div
|
||||
class="hidden lg:relative lg:col-start-2 lg:row-start-1 lg:row-end-4 lg:block"
|
||||
>
|
||||
<img
|
||||
src="@/assets/wallpaper.jpg"
|
||||
alt=""
|
||||
class="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref<string | undefined>();
|
||||
|
||||
let offerManualTimeout: NodeJS.Timeout | undefined;
|
||||
const offerManual = ref(false);
|
||||
const manualToken = ref("");
|
||||
const manualLoading = ref(false);
|
||||
|
||||
async function auth() {
|
||||
await invoke("auth_initiate");
|
||||
}
|
||||
|
||||
function authWrapper_wrapper() {
|
||||
error.value = undefined;
|
||||
loading.value = true;
|
||||
auth().catch((e) => {
|
||||
loading.value = false;
|
||||
error.value = e;
|
||||
if (offerManualTimeout) clearTimeout(offerManualTimeout);
|
||||
});
|
||||
offerManualTimeout = setTimeout(() => {
|
||||
offerManual.value = true;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function continueManual() {
|
||||
await invoke("manual_recieve_handshake", { token: manualToken.value });
|
||||
}
|
||||
|
||||
function continueManual_wrapper() {
|
||||
loading.value = true;
|
||||
continueManual()
|
||||
.catch((e) => {
|
||||
error.value = e;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
196
main/components/LibrarySearch.vue
Normal file
196
main/components/LibrarySearch.vue
Normal file
@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div
|
||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||
>
|
||||
<MagnifyingGlassIcon
|
||||
class="h-5 w-5 text-zinc-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||
placeholder="Search library..."
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@click="() => calculateGames(true)"
|
||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
|
||||
>
|
||||
<ArrowPathIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||
<NuxtLink
|
||||
v-for="nav in filteredNavigation"
|
||||
:key="nav.id"
|
||||
:class="[
|
||||
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
||||
nav.index === currentNavigation
|
||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||
: nav.isInstalled.value
|
||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||
]"
|
||||
:href="nav.route"
|
||||
>
|
||||
<div class="flex items-center w-full gap-x-3">
|
||||
<div
|
||||
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
||||
>
|
||||
<img
|
||||
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
||||
:src="icons[nav.id]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<p
|
||||
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
|
||||
>
|
||||
{{ nav.label }}
|
||||
</p>
|
||||
<p
|
||||
class="text-xs font-medium"
|
||||
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
|
||||
>
|
||||
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
|
||||
import { TransitionGroup } from "vue";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
// Style information
|
||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Installed]: "text-green-500",
|
||||
[GameStatusEnum.Downloading]: "text-blue-500",
|
||||
[GameStatusEnum.Validating]: "text-blue-300",
|
||||
[GameStatusEnum.Running]: "text-green-500",
|
||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||
[GameStatusEnum.Queued]: "text-blue-500",
|
||||
[GameStatusEnum.Updating]: "text-blue-500",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
||||
};
|
||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Not installed",
|
||||
[GameStatusEnum.Queued]: "Queued",
|
||||
[GameStatusEnum.Downloading]: "Downloading...",
|
||||
[GameStatusEnum.Validating]: "Validating...",
|
||||
[GameStatusEnum.Installed]: "Installed",
|
||||
[GameStatusEnum.Updating]: "Updating...",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||
[GameStatusEnum.SetupRequired]: "Setup required",
|
||||
[GameStatusEnum.Running]: "Running",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const searchQuery = ref("");
|
||||
|
||||
const games: {
|
||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||
} = {};
|
||||
const icons: { [key: string]: string } = {};
|
||||
|
||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||
|
||||
async function calculateGames(clearAll = false) {
|
||||
if (clearAll) rawGames.value = [];
|
||||
// If we update immediately, the navigation gets re-rendered before we
|
||||
// add all the necessary state, and it freaks tf out
|
||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||
for (const game of newGames) {
|
||||
if (games[game.id]) continue;
|
||||
games[game.id] = await useGame(game.id);
|
||||
}
|
||||
for (const game of newGames) {
|
||||
if (icons[game.id]) continue;
|
||||
icons[game.id] = await useObject(game.mIconObjectId);
|
||||
}
|
||||
rawGames.value = newGames;
|
||||
}
|
||||
|
||||
await calculateGames();
|
||||
|
||||
const navigation = computed(() =>
|
||||
rawGames.value.map((game) => {
|
||||
const status = games[game.id].status;
|
||||
|
||||
const isInstalled = computed(
|
||||
() =>
|
||||
status.value.type == GameStatusEnum.Installed ||
|
||||
status.value.type == GameStatusEnum.SetupRequired
|
||||
);
|
||||
|
||||
const item = {
|
||||
label: game.mName,
|
||||
route: `/library/${game.id}`,
|
||||
prefix: `/library/${game.id}`,
|
||||
isInstalled,
|
||||
id: game.id,
|
||||
};
|
||||
return item;
|
||||
})
|
||||
);
|
||||
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(
|
||||
navigation.value
|
||||
);
|
||||
|
||||
const filteredNavigation = computed(() => {
|
||||
if (!searchQuery.value)
|
||||
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return navigation.value
|
||||
.filter((nav) => nav.label.toLowerCase().includes(query))
|
||||
.map((e, i) => ({ ...e, index: i }));
|
||||
});
|
||||
|
||||
listen("update_library", async (event) => {
|
||||
console.log("Updating library");
|
||||
let oldNavigation = navigation.value[currentNavigation.value];
|
||||
await calculateGames();
|
||||
recalculateNavigation();
|
||||
if (oldNavigation !== navigation.value[currentNavigation.value]) {
|
||||
console.log("Triggered");
|
||||
router.push("/library");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
7
main/components/LoadingIndicator.vue
Normal file
7
main/components/LoadingIndicator.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const loading = useLoadingIndicator();
|
||||
|
||||
watch(loading.isLoading, console.log);
|
||||
</script>
|
||||
7
main/components/Logo.vue
Normal file
7
main/components/Logo.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg class="text-blue-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 13.5C4 11.0008 5.38798 8.76189 7.00766 7C8.43926 5.44272 10.0519 4.25811 11.0471 3.5959C11.6287 3.20893 12.3713 3.20893 12.9529 3.5959C13.9481 4.25811 15.5607 5.44272 16.9923 7C18.612 8.76189 20 11.0008 20 13.5C20 17.9183 16.4183 21.5 12 21.5C7.58172 21.5 4 17.9183 4 13.5Z"
|
||||
stroke="currentColor" stroke-width="2" />
|
||||
</svg>
|
||||
</template>
|
||||
16
main/components/MiniHeader.vue
Normal file
16
main/components/MiniHeader.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||
>
|
||||
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
|
||||
<Wordmark class="mt-1" />
|
||||
</div>
|
||||
<WindowControl />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
const window = getCurrentWindow();
|
||||
</script>
|
||||
17
main/components/OfflineHeaderWidget.vue
Normal file
17
main/components/OfflineHeaderWidget.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowDownTrayIcon, CloudIcon } from "@heroicons/vue/20/solid";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="transition inline-flex items-center rounded-sm px-4 py-1.5 bg-zinc-900 text-sm text-zinc-400 gap-x-2"
|
||||
>
|
||||
<div class="relative">
|
||||
<CloudIcon class="h-5 z-50 text-zinc-500" />
|
||||
<div
|
||||
class="absolute rounded-full left-1/2 top-1/2 -translate-y-[45%] -translate-x-1/2 w-[2px] h-6 rotate-[45deg] bg-zinc-400 z-50"
|
||||
/>
|
||||
</div>
|
||||
Offline
|
||||
</div>
|
||||
</template>
|
||||
7
main/components/PageWidget.vue
Normal file
7
main/components/PageWidget.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
class="inline-flex items-center gap-x-2 px-1 py-0.5 rounded bg-blue-900 text-zinc-100 hover:bg-blue-800"
|
||||
>
|
||||
<slot />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
24
main/components/WindowControl.vue
Normal file
24
main/components/WindowControl.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<HeaderButton v-if="showMinimise" @click="() => minimise()">
|
||||
<MinusIcon />
|
||||
</HeaderButton>
|
||||
<HeaderButton @click="() => close()">
|
||||
<XMarkIcon />
|
||||
</HeaderButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
const window = getCurrentWindow();
|
||||
const showMinimise = await window.isMinimizable();
|
||||
|
||||
async function close() {
|
||||
await window.close();
|
||||
}
|
||||
|
||||
async function minimise() {
|
||||
await window.minimize();
|
||||
}
|
||||
</script>
|
||||
11
main/components/Wordmark.vue
Normal file
11
main/components/Wordmark.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="inline-flex justify-center items-center gap-x-1 -mb-1 relative">
|
||||
<svg aria-hidden="true" viewBox="0 0 418 42" class="absolute inset-0 h-full w-full fill-blue-300/30 scale-75"
|
||||
preserveAspectRatio="none">
|
||||
<path
|
||||
d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
|
||||
</svg>
|
||||
<Logo class="h-6" />
|
||||
<span class="text-blue-400 font-display font-bold text-xl uppercase">Drop</span>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user