mirror of
https://github.com/swingmx/webclient.git
synced 2025-12-24 19:30:20 +00:00
create initial onboarding draft
This commit is contained in:
193
src/App.vue
193
src/App.vue
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<ContextMenu />
|
||||
<Modal />
|
||||
<Notification />
|
||||
<ContextMenu v-if="!hideUI" />
|
||||
<Modal v-if="!hideUI" />
|
||||
<Notification v-if="!hideUI" />
|
||||
<div id="drag-img" class="ellip2" style=""></div>
|
||||
<section
|
||||
v-if="!hideUI"
|
||||
id="app-grid"
|
||||
:class="{
|
||||
useSidebar: settings.use_sidebar && xl,
|
||||
@@ -27,153 +28,187 @@
|
||||
<BottomBar />
|
||||
<!-- <BubbleManager /> -->
|
||||
</section>
|
||||
<div v-else id="noui">
|
||||
<BalancerProvider>
|
||||
<RouterView />
|
||||
</BalancerProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// @libraries
|
||||
import { vElementSize } from "@vueuse/components";
|
||||
import { onStartTyping } from "@vueuse/core";
|
||||
import { onMounted, Ref, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { BalancerProvider } from "vue-wrap-balancer";
|
||||
import { vElementSize } from '@vueuse/components'
|
||||
import { onStartTyping } from '@vueuse/core'
|
||||
import { onBeforeMount, onMounted, Ref, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { BalancerProvider } from 'vue-wrap-balancer'
|
||||
|
||||
// @stores
|
||||
import useAuth from "@/stores/auth";
|
||||
import { content_height, content_width, isMobile, resizer_width, updateCardWidth } from "@/stores/content-width";
|
||||
import useLyrics from "@/stores/lyrics";
|
||||
import useModal from "@/stores/modal";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useSettings from "@/stores/settings";
|
||||
import useTracker from "@/stores/tracker";
|
||||
import useAuth from '@/stores/auth'
|
||||
import { content_height, content_width, isMobile, resizer_width, updateCardWidth } from '@/stores/content-width'
|
||||
import useLyrics from '@/stores/lyrics'
|
||||
import useModal from '@/stores/modal'
|
||||
import useQueue from '@/stores/queue'
|
||||
import useSettings from '@/stores/settings'
|
||||
import useTracker from '@/stores/tracker'
|
||||
|
||||
// @utils
|
||||
import handleShortcuts from "@/helpers/useKeyboard";
|
||||
import { xl, xxl } from "./composables/useBreakpoints";
|
||||
import handleShortcuts from '@/helpers/useKeyboard'
|
||||
import { xl, xxl } from './composables/useBreakpoints'
|
||||
|
||||
// @small-components
|
||||
import ContextMenu from "@/components/ContextMenu.vue";
|
||||
import Modal from "@/components/modal.vue";
|
||||
import Notification from "@/components/Notification.vue";
|
||||
import ContextMenu from '@/components/ContextMenu.vue'
|
||||
import Modal from '@/components/modal.vue'
|
||||
import Notification from '@/components/Notification.vue'
|
||||
|
||||
// @app-grid-components
|
||||
import BottomBar from "@/components/BottomBar/BottomBar.vue";
|
||||
import LeftSidebar from "@/components/LeftSidebar/index.vue";
|
||||
import NavBar from "@/components/nav/NavBar.vue";
|
||||
import RightSideBar from "@/components/RightSideBar/Main.vue";
|
||||
import BottomBar from '@/components/BottomBar/BottomBar.vue'
|
||||
import LeftSidebar from '@/components/LeftSidebar/index.vue'
|
||||
import NavBar from '@/components/nav/NavBar.vue'
|
||||
import RightSideBar from '@/components/RightSideBar/Main.vue'
|
||||
|
||||
import { getAllSettings } from "@/requests/settings";
|
||||
import { getRootDirs } from "@/requests/settings/rootdirs";
|
||||
import { getLoggedInUser } from "./requests/auth";
|
||||
import { getAllSettings } from '@/requests/settings'
|
||||
import { getRootDirs } from '@/requests/settings/rootdirs'
|
||||
import { getLoggedInUser } from './requests/auth'
|
||||
// import BubbleManager from "./components/bubbles/BinManager.vue";
|
||||
|
||||
const appcontent: Ref<HTMLLegendElement | null> = ref(null);
|
||||
const auth = useAuth();
|
||||
const queue = useQueue();
|
||||
const modal = useModal();
|
||||
const lyrics = useLyrics();
|
||||
const router = useRouter();
|
||||
const settings = useSettings();
|
||||
useTracker();
|
||||
const appcontent: Ref<HTMLLegendElement | null> = ref(null)
|
||||
const auth = useAuth()
|
||||
const queue = useQueue()
|
||||
const modal = useModal()
|
||||
const lyrics = useLyrics()
|
||||
const router = useRouter()
|
||||
const settings = useSettings()
|
||||
const hideUI = ref(false)
|
||||
useTracker()
|
||||
|
||||
handleShortcuts(useQueue, useModal);
|
||||
handleShortcuts(useQueue, useModal)
|
||||
|
||||
router.afterEach(() => {
|
||||
(document.getElementById("acontent") as HTMLElement).scrollTo(0, 0);
|
||||
});
|
||||
const acontent = document.getElementById('acontent') as HTMLElement
|
||||
|
||||
if (acontent) {
|
||||
acontent.scrollTo(0, 0)
|
||||
}
|
||||
})
|
||||
|
||||
onStartTyping(() => {
|
||||
const elem = document.getElementById("globalsearch") as HTMLInputElement;
|
||||
elem.focus();
|
||||
elem.value = "";
|
||||
});
|
||||
const elem = document.getElementById('globalsearch') as HTMLInputElement
|
||||
elem.focus()
|
||||
elem.value = ''
|
||||
})
|
||||
|
||||
function getContentSize() {
|
||||
const elem = document.getElementById("acontent") as HTMLElement;
|
||||
const elem = document.getElementById('acontent') as HTMLElement
|
||||
return {
|
||||
width: elem.offsetWidth,
|
||||
height: elem.offsetHeight,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateContentElemSize({ width, height }: { width: number; height: number }) {
|
||||
// 1572 is the maxwidth of the #acontent. see app-grid.scss > $maxwidth
|
||||
const elem_width = appcontent.value?.offsetWidth || 0;
|
||||
const elem_width = appcontent.value?.offsetWidth || 0
|
||||
|
||||
content_width.value = elem_width;
|
||||
content_height.value = height;
|
||||
content_width.value = elem_width
|
||||
content_height.value = height
|
||||
|
||||
resizer_width.value = elem_width;
|
||||
updateCardWidth();
|
||||
resizer_width.value = elem_width
|
||||
updateCardWidth()
|
||||
}
|
||||
|
||||
function handleRootDirsPrompt() {
|
||||
getRootDirs().then(dirs => {
|
||||
if (dirs.length === 0) {
|
||||
modal.showRootDirsPromptModal();
|
||||
modal.showRootDirsPromptModal()
|
||||
} else {
|
||||
settings.setRootDirs(dirs);
|
||||
settings.setRootDirs(dirs)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const getCookieValue = (name: string) => document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''
|
||||
|
||||
onMounted(async () => {
|
||||
const { width, height } = getContentSize();
|
||||
updateContentElemSize({ width, height });
|
||||
|
||||
const res = await getLoggedInUser();
|
||||
|
||||
if (res.status == 200) {
|
||||
auth.setUser(res.data);
|
||||
} else {
|
||||
return;
|
||||
if (hideUI.value) {
|
||||
return
|
||||
}
|
||||
|
||||
settings.initializeVolume();
|
||||
const { width, height } = getContentSize()
|
||||
updateContentElemSize({ width, height })
|
||||
|
||||
handleRootDirsPrompt();
|
||||
const res = await getLoggedInUser()
|
||||
|
||||
if (res.status == 200) {
|
||||
auth.setUser(res.data)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
settings.initializeVolume()
|
||||
|
||||
handleRootDirsPrompt()
|
||||
|
||||
getAllSettings()
|
||||
.then(({ settings: data }) => {
|
||||
settings.mapDbSettings(data);
|
||||
settings.mapDbSettings(data)
|
||||
})
|
||||
.then(() => {
|
||||
if (queue.currenttrack && !settings.use_lyrics_plugin) {
|
||||
lyrics.checkExists(queue.currenttrack.filepath, queue.currenttrack.trackhash);
|
||||
lyrics.checkExists(queue.currenttrack.filepath, queue.currenttrack.trackhash)
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
const onboardingComplete = getCookieValue('onboarding_complete')
|
||||
console.log('onboardingComplete', onboardingComplete)
|
||||
|
||||
if (!onboardingComplete) {
|
||||
hideUI.value = true
|
||||
router.push({
|
||||
name: Routes.Onboarding,
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
// Detect OS & browser agents and add class
|
||||
import { defineComponent } from "vue";
|
||||
import usePlayer from "./composables/usePlayer";
|
||||
import { defineComponent } from 'vue'
|
||||
import usePlayer from './composables/usePlayer'
|
||||
import { Routes } from './router'
|
||||
export default defineComponent({
|
||||
name: "OsAndBrowserSpecificContent",
|
||||
name: 'OsAndBrowserSpecificContent',
|
||||
mounted() {
|
||||
this.applyClassBasedOnAgent();
|
||||
this.applyClassBasedOnAgent()
|
||||
},
|
||||
methods: {
|
||||
applyClassBasedOnAgent() {
|
||||
const userAgent = navigator.userAgent;
|
||||
const isWindows = /Win/.test(userAgent);
|
||||
const isLinux = /Linux/.test(userAgent) && !/Android/.test(userAgent);
|
||||
const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
const userAgent = navigator.userAgent
|
||||
const isWindows = /Win/.test(userAgent)
|
||||
const isLinux = /Linux/.test(userAgent) && !/Android/.test(userAgent)
|
||||
const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor)
|
||||
if ((isWindows || isLinux) && isChrome) {
|
||||
document.documentElement.classList.add("designatedOS");
|
||||
document.documentElement.classList.add('designatedOS')
|
||||
} else {
|
||||
document.documentElement.classList.add("otherOS");
|
||||
document.documentElement.classList.add('otherOS')
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./assets/scss/mixins.scss";
|
||||
@import './assets/scss/mixins.scss';
|
||||
.designatedOS .r-sidebar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#noui {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
4
src/assets/icons/add.svg
Normal file
4
src/assets/icons/add.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 28 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.105 26.21C20.7369 26.21 26.2121 20.7273 26.2121 14.105C26.2121 7.47312 20.7273 2 14.0954 2C7.47523 2 2 7.47312 2 14.105C2 20.7273 7.48484 26.21 14.105 26.21ZM14.105 23.8255C8.71085 23.8255 4.39412 19.4991 4.39412 14.105C4.39412 8.71085 8.70124 4.38452 14.0954 4.38452C19.4895 4.38452 23.8276 8.71085 23.8276 14.105C23.8276 19.4991 19.4991 23.8255 14.105 23.8255Z" fill="currentColor"/>
|
||||
<path d="M8.68359 14.1029C8.68359 14.7383 9.13265 15.1819 9.78304 15.1819H12.9963V18.4048C12.9963 19.0456 13.4496 19.5043 14.085 19.5043C14.7321 19.5043 15.1929 19.0552 15.1929 18.4048V15.1819H18.4179C19.0586 15.1819 19.5152 14.7383 19.5152 14.1029C19.5152 13.4558 19.0586 12.995 18.4179 12.995H15.1929V9.78167C15.1929 9.12917 14.7321 8.67261 14.085 8.67261C13.4496 8.67261 12.9963 9.12917 12.9963 9.78167V12.995H9.78304C9.13265 12.995 8.68359 13.4558 8.68359 14.1029Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 979 B |
@@ -1,4 +1,4 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.84421 24.8972H21.0295C23.5685 24.8972 24.8737 23.5919 24.8737 21.0914V6.82921C24.8737 4.32656 23.5685 3.02344 21.0295 3.02344H6.84421C4.31484 3.02344 3 4.31695 3 6.82921V21.0914C3 23.6016 4.31484 24.8972 6.84421 24.8972Z" fill="white"/>
|
||||
<path d="M12.6617 19.7301C12.219 19.7301 11.8571 19.5387 11.5314 19.1137L8.65744 15.6204C8.45002 15.3523 8.34033 15.0818 8.34033 14.7848C8.34033 14.1879 8.81588 13.7037 9.42033 13.7037C9.7822 13.7037 10.0611 13.8281 10.3646 14.2148L12.6212 17.0827L17.4669 9.32416C17.7233 8.92409 18.0554 8.71338 18.4225 8.71338C18.9981 8.71338 19.5376 9.12401 19.5376 9.73807C19.5376 10.0085 19.4033 10.2949 19.232 10.5642L13.7474 19.1053C13.4802 19.5152 13.0982 19.7301 12.6617 19.7301Z" fill="blue"/>
|
||||
<svg viewBox="0 0 28 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.84421 24.8972H21.0295C23.5685 24.8972 24.8737 23.5919 24.8737 21.0914V6.82921C24.8737 4.32656 23.5685 3.02344 21.0295 3.02344H6.84421C4.31484 3.02344 3 4.31695 3 6.82921V21.0914C3 23.6016 4.31484 24.8972 6.84421 24.8972Z" fill="transparent"/>
|
||||
<path d="M12.6617 19.7301C12.219 19.7301 11.8571 19.5387 11.5314 19.1137L8.65744 15.6204C8.45002 15.3523 8.34033 15.0818 8.34033 14.7848C8.34033 14.1879 8.81588 13.7037 9.42033 13.7037C9.7822 13.7037 10.0611 13.8281 10.3646 14.2148L12.6212 17.0827L17.4669 9.32416C17.7233 8.92409 18.0554 8.71338 18.4225 8.71338C18.9981 8.71338 19.5376 9.12401 19.5376 9.73807C19.5376 10.0085 19.4033 10.2949 19.232 10.5642L13.7474 19.1053C13.4802 19.5152 13.0982 19.7301 12.6617 19.7301Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 839 B |
4
src/assets/icons/subtract.svg
Normal file
4
src/assets/icons/subtract.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 28 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.105 26.21C20.7369 26.21 26.2121 20.7273 26.2121 14.105C26.2121 7.47312 20.7273 2 14.0954 2C7.47523 2 2 7.47312 2 14.105C2 20.7273 7.48484 26.21 14.105 26.21ZM14.105 23.8255C8.71085 23.8255 4.39412 19.4991 4.39412 14.105C4.39412 8.71085 8.70124 4.38452 14.0954 4.38452C19.4895 4.38452 23.8276 8.71085 23.8276 14.105C23.8276 19.4991 19.4991 23.8255 14.105 23.8255Z" fill="currentColor"/>
|
||||
<path d="M9.64882 15.215H18.5527C19.2532 15.215 19.7386 14.8023 19.7386 14.1263C19.7386 13.4408 19.2725 13.0184 18.5527 13.0184H9.64882C8.92906 13.0184 8.45117 13.4408 8.45117 14.1263C8.45117 14.8023 8.94828 15.215 9.64882 15.215Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 741 B |
@@ -75,7 +75,7 @@ button {
|
||||
font-family: "SF Compact Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 0.9rem !important;
|
||||
font-weight: 700;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: inherit;
|
||||
border-radius: $small;
|
||||
@@ -83,9 +83,9 @@ button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.25rem;
|
||||
padding: 0 $small;
|
||||
height: 2rem;
|
||||
transition: background-color 0.2s ease-out, color 0.2s ease-out, border 0.2s ease-out;
|
||||
padding: $small 1rem;
|
||||
|
||||
background-color: $gray4;
|
||||
cursor: pointer;
|
||||
|
||||
152
src/components/Onboarding/Account.vue
Normal file
152
src/components/Onboarding/Account.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="account">
|
||||
<div class="avatarbox rounded-sm">
|
||||
<!-- <Avatar :name="username"/> -->
|
||||
<!-- <LogoSvg /> -->
|
||||
</div>
|
||||
<form class="createadmin" @submit.prevent="createAccount">
|
||||
<div>
|
||||
<div class="heading">Create admin account</div>
|
||||
<div class="description">This account will be used to manage your server.</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="form">
|
||||
<div class="names">
|
||||
<label for="username">Username</label>
|
||||
<Input :placeholder="username" input-id="username" required @input="input => (username = input)" />
|
||||
</div>
|
||||
<div class="passwords">
|
||||
<div class="names">
|
||||
<label for="password">Password</label>
|
||||
<Input
|
||||
:placeholder="password"
|
||||
type="password"
|
||||
input-id="password"
|
||||
required
|
||||
@input="input => (password = input)"
|
||||
/>
|
||||
</div>
|
||||
<div class="names">
|
||||
<label for="confirmPassword">Confirm Password</label>
|
||||
<Input
|
||||
:placeholder="confirmPassword"
|
||||
type="password"
|
||||
input-id="confirmPassword"
|
||||
required
|
||||
@input="input => (confirmPassword = input)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-continue">Create account</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { addNewUser } from '@/requests/auth'
|
||||
import Input from '@/components/shared/Input.vue'
|
||||
|
||||
const username = ref('admin')
|
||||
const password = ref('✶✶✶✶✶✶✶✶')
|
||||
const confirmPassword = ref('✶✶✶✶✶✶✶✶')
|
||||
|
||||
const emit = defineEmits(['accountCreated', 'error'])
|
||||
|
||||
function validatePassword() {
|
||||
// check if password is at least 8 characters
|
||||
if (password.value.length < 8) {
|
||||
return emit('error', 'Password must be at least 8 characters')
|
||||
}
|
||||
|
||||
// check if password and confirm password match
|
||||
if (password.value !== confirmPassword.value) {
|
||||
return emit('error', 'Passwords do not match')
|
||||
}
|
||||
|
||||
emit('error', '')
|
||||
return true
|
||||
}
|
||||
|
||||
async function createAccount() {
|
||||
if (!validatePassword()) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await addNewUser({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
})
|
||||
|
||||
console.log(response)
|
||||
|
||||
if (response.status === 200) {
|
||||
emit('accountCreated', response.data.userhome)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// focus on username input
|
||||
document.getElementById('username')?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.account {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.25fr;
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
margin: 0 $medium 0 $medium;
|
||||
|
||||
.avatarbox {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// background gradient
|
||||
background: linear-gradient(37deg, $pink, $purple);
|
||||
display: flex;
|
||||
// margin-left: $medium;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $small;
|
||||
|
||||
svg {
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
background: url('https://images.unsplash.com/photo-1632516643720-e7f5d7d6ecc9?q=80&w=2022&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')
|
||||
no-repeat center center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.passwords {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
margin-top: $small;
|
||||
}
|
||||
|
||||
.createadmin {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.heading {
|
||||
margin-bottom: $smaller;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: rgb(210, 210, 210);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
354
src/components/Onboarding/FilePicker.vue
Normal file
354
src/components/Onboarding/FilePicker.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="file-picker">
|
||||
<div class="input-container">
|
||||
<div class="head">
|
||||
<button class="btn-back" @click="$emit('cancel')">
|
||||
<ArrowLeftSvg height="1.2rem" />
|
||||
</button>
|
||||
<div>
|
||||
<div class="breadcrumb">
|
||||
<div
|
||||
v-for="path in subPaths"
|
||||
:key="path.path"
|
||||
class="bitem"
|
||||
@click="() => fetchFolders(path.path)"
|
||||
>
|
||||
{{ path.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
><button
|
||||
class="btn-finish"
|
||||
@click="
|
||||
$emit(
|
||||
'submitDirs',
|
||||
Array.from(selectedFolders.values()).map(index => folders[index].path)
|
||||
)
|
||||
"
|
||||
>
|
||||
Continue
|
||||
</button></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="folders">
|
||||
<div
|
||||
v-for="(folder, index) in renderedFolders"
|
||||
:key="folder.path"
|
||||
class="folder"
|
||||
:class="{
|
||||
selected: selectedFolders.has(index),
|
||||
'selected-first': selectedFolders.has(index) && isFirstSelected(index),
|
||||
'selected-last': selectedFolders.has(index) && isLastSelected(index),
|
||||
}"
|
||||
@click.exact="handleSelect(index, false, false)"
|
||||
@click.meta="handleSelect(index, false, true)"
|
||||
@click.ctrl="handleSelect(index, false, true)"
|
||||
@click.shift="handleSelect(index, true, false)"
|
||||
@dblclick="fetchFolders(folder.path)"
|
||||
>
|
||||
<FolderSvg />
|
||||
<span>{{ folder.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help rounded-sm">
|
||||
<div class="help-content">
|
||||
<InfoSvg />
|
||||
<span>Use (⌘/Ctrl or Shift) + Click to select multiple folders</span>
|
||||
</div>
|
||||
<span>{{ selectedFolders.size }} Selected</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- TODO: Handle duplicates on final root dirs -->
|
||||
<!-- TODO: Clear Errors on setting root dirs -->
|
||||
<!-- TODO: Work on breadcrumb -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Input from '../shared/Input.vue'
|
||||
import { getFolders } from '@/requests/settings/rootdirs'
|
||||
import FolderSvg from '@/assets/icons/folder.svg'
|
||||
import AddSvg from '@/assets/icons/add.svg'
|
||||
import SubtractSvg from '@/assets/icons/subtract.svg'
|
||||
import ArrowLeftSvg from '@/assets/icons/arrow.svg'
|
||||
import InfoSvg from '@/assets/icons/info.svg'
|
||||
import { Folder } from '@/interfaces'
|
||||
import { createSubPaths } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
userhome: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'submitDirs', dirs: string[]): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const currentPath = ref<string>('')
|
||||
const folders = ref<Folder[]>([])
|
||||
const subPaths = computed(() => {
|
||||
return createSubPaths(currentPath.value, '')[1]
|
||||
})
|
||||
const renderedFolders = computed(() => {
|
||||
const first2 = [...folders.value]
|
||||
|
||||
if (first2.length >= 2) {
|
||||
first2[0].name = '↑'
|
||||
first2[1].name = '. (this folder)'
|
||||
}
|
||||
|
||||
return first2
|
||||
})
|
||||
|
||||
const lastSelectedIndex = ref<number>(-1)
|
||||
const selectedFolders = ref<Set<number>>(new Set())
|
||||
|
||||
function isFirstSelected(index: number): boolean {
|
||||
if (!selectedFolders.value.has(index)) return false
|
||||
const previousIndex = index - 1
|
||||
return previousIndex < 0 || !selectedFolders.value.has(previousIndex)
|
||||
}
|
||||
|
||||
function isLastSelected(index: number): boolean {
|
||||
if (!selectedFolders.value.has(index)) return false
|
||||
const nextIndex = index + 1
|
||||
return nextIndex >= renderedFolders.value.length || !selectedFolders.value.has(nextIndex)
|
||||
}
|
||||
|
||||
async function fetchFolders(folder: string) {
|
||||
const results = await getFolders(folder)
|
||||
folders.value = results
|
||||
currentPath.value = folder
|
||||
selectedFolders.value.clear()
|
||||
lastSelectedIndex.value = -1
|
||||
}
|
||||
|
||||
function handleSelect(index: number, shift: boolean, ctrl: boolean) {
|
||||
// INFO: Handle shift - range selection
|
||||
if (shift) {
|
||||
// INFO: Handle selection of parent and current folder
|
||||
// INFO: .. and . folder should only be selected alone
|
||||
// INFO: Handle selection from ../. to folder N
|
||||
if (lastSelectedIndex.value <= 1) {
|
||||
lastSelectedIndex.value = 2
|
||||
selectedFolders.value = new Set([2])
|
||||
}
|
||||
|
||||
// INFO: Handle selection from folder N to parent
|
||||
if (index <= 1) {
|
||||
selectedFolders.value = new Set([lastSelectedIndex.value])
|
||||
index = 2
|
||||
}
|
||||
|
||||
// Select range from last selected to current
|
||||
const start = Math.min(lastSelectedIndex.value, index)
|
||||
const end = Math.max(lastSelectedIndex.value, index)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
selectedFolders.value.add(i)
|
||||
}
|
||||
|
||||
lastSelectedIndex.value = index
|
||||
return
|
||||
}
|
||||
|
||||
// INFO: Handle ctrl - toggle selection
|
||||
if (ctrl) {
|
||||
if (index <= 1) {
|
||||
selectedFolders.value = new Set([index])
|
||||
lastSelectedIndex.value = index
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedFolders.value.has(0) || selectedFolders.value.has(1)) {
|
||||
selectedFolders.value = new Set([index])
|
||||
lastSelectedIndex.value = index
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedFolders.value.has(index)) {
|
||||
selectedFolders.value.delete(index)
|
||||
} else {
|
||||
selectedFolders.value.add(index)
|
||||
}
|
||||
|
||||
lastSelectedIndex.value = index
|
||||
return
|
||||
}
|
||||
|
||||
// INFO: Handle regular click - single selection
|
||||
selectedFolders.value.clear()
|
||||
selectedFolders.value.add(index)
|
||||
lastSelectedIndex.value = index
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
currentPath.value = props.userhome
|
||||
|
||||
await fetchFolders(props.userhome)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.file-picker {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: max-content 1fr max-content;
|
||||
position: relative;
|
||||
gap: $small;
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $small;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.bitem {
|
||||
color: rgb(155, 154, 154);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
// outline: solid 1px;
|
||||
}
|
||||
|
||||
.bitem:last-child {
|
||||
color: $blue;
|
||||
cursor: default;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bitem::after {
|
||||
content: '/';
|
||||
margin-right: $small;
|
||||
color: $gray2;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
|
||||
position: absolute;
|
||||
// center the before vertical
|
||||
top: 50%;
|
||||
right: -0.9rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.head {
|
||||
margin-bottom: $small;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
button {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
// margin: $small $small;
|
||||
font-size: 0.65rem;
|
||||
|
||||
.help-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $small;
|
||||
}
|
||||
|
||||
background-color: transparent;
|
||||
padding: $small;
|
||||
margin-bottom: -$small;
|
||||
color: $gray1;
|
||||
|
||||
svg {
|
||||
height: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.folder:nth-child(2),
|
||||
.folder:first-child {
|
||||
font-size: 0.7rem;
|
||||
color: $gray1;
|
||||
|
||||
svg {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.folder:nth-child(2) {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.folders {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: calc(100% + $small);
|
||||
overflow-y: auto;
|
||||
margin-left: $smaller;
|
||||
|
||||
.folder {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
align-items: center;
|
||||
gap: $small;
|
||||
padding: $small;
|
||||
cursor: default;
|
||||
border-radius: $smaller;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
|
||||
// disable select
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
||||
width: calc(100% - 1rem);
|
||||
|
||||
&:hover {
|
||||
background-color: $gray5;
|
||||
|
||||
.action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folder.selected {
|
||||
background-color: $darkblue;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.folder.selected-first {
|
||||
border-top-left-radius: $small;
|
||||
border-top-right-radius: $small;
|
||||
}
|
||||
|
||||
.folder.selected-last {
|
||||
border-bottom-left-radius: $small;
|
||||
border-bottom-right-radius: $small;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
251
src/components/Onboarding/RootDirs.vue
Normal file
251
src/components/Onboarding/RootDirs.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<FilePicker v-if="showFilePicker" :userhome="userhome" @submitDirs="handleSubmitDirs" @cancel="toggleFilePicker" />
|
||||
<div v-else class="rootdirconfig">
|
||||
<div class="heading">Configure root directories</div>
|
||||
<div class="description">Where do you want to look for music?</div>
|
||||
<br />
|
||||
<div class="options">
|
||||
<div class="option" @click="toggleHomeDir">
|
||||
<div>
|
||||
<div class="option-title">Home directory</div>
|
||||
<div class="option-description">
|
||||
Scan all folders in <span class="userhome">{{ userhome }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-selected">
|
||||
<CheckSvg v-if="homeDirSelected" height="1.75rem" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="option" @click="toggleFilePicker">
|
||||
<div>
|
||||
<div class="option-title">Specific directory</div>
|
||||
<div class="option-description">
|
||||
{{
|
||||
specificDirsSelected
|
||||
? `${finalRootDirs.length} folder${finalRootDirs.length !== 1 ? 's' : ''} selected`
|
||||
: 'Select folder to scan for music'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="specificDirsSelected" class="option-selected">
|
||||
<span>Add Folders</span>
|
||||
<CheckSvg v-if="specificDirsSelected" height="1.75rem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="btn-container">
|
||||
<button class="btn-continue" @click="handleContinue">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="rootDirs.length > 0 && !homeDirSelected" class="selected-folders rounded-sm">
|
||||
<div class="heading">{{ finalRootDirs.length }} Selected Folders</div>
|
||||
<div class="folders">
|
||||
<div v-for="folder in finalRootDirs" :key="folder" class="folder">
|
||||
<FolderSvg />
|
||||
{{ folder.startsWith(userhome) ? folder.replace(userhome, '~') : folder }}
|
||||
<div class="action" @click="handleRemoveFolder(folder)">
|
||||
<SubtractSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import FilePicker from './FilePicker.vue'
|
||||
import CheckSvg from '@/assets/icons/check.filled.svg'
|
||||
import FolderSvg from '@/assets/icons/folder.svg'
|
||||
import SubtractSvg from '@/assets/icons/subtract.svg'
|
||||
|
||||
// SECTION: Props & Emits
|
||||
const props = defineProps<{
|
||||
userhome: string
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'setRootDirs', dirs: string[]): void
|
||||
(e: 'error', error: string): void
|
||||
}>()
|
||||
|
||||
// SECTION: Properties
|
||||
const showFilePicker = ref(false)
|
||||
const rootDirs = ref<string[]>([])
|
||||
const finalRootDirs = computed(() => {
|
||||
if (rootDirs.value.length <= 1) {
|
||||
return rootDirs.value
|
||||
}
|
||||
|
||||
// Remove duplicates first
|
||||
const uniqueDirs = [...new Set(rootDirs.value)]
|
||||
|
||||
// Sort directories by length (shortest first) to process parents before children
|
||||
const sortedDirs = uniqueDirs.sort((a, b) => a.length - b.length)
|
||||
const filteredDirs: string[] = []
|
||||
|
||||
for (const dir of sortedDirs) {
|
||||
// Check if this directory is a child of any already selected directory
|
||||
const isChild = filteredDirs.some(parentDir => {
|
||||
// Ensure parent directory ends with '/' for proper path comparison
|
||||
const normalizedParent = parentDir.endsWith('/') ? parentDir : parentDir + '/'
|
||||
return dir.startsWith(normalizedParent)
|
||||
})
|
||||
|
||||
// Only add if it's not a child of any parent directory
|
||||
if (!isChild) {
|
||||
filteredDirs.push(dir)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredDirs
|
||||
})
|
||||
const homeDirSelected = computed(() => {
|
||||
return finalRootDirs.value.length == 1 && finalRootDirs.value[0] == props.userhome
|
||||
})
|
||||
const specificDirsSelected = computed(() => finalRootDirs.value.length && !homeDirSelected.value)
|
||||
|
||||
// SECTION: Handlers
|
||||
function toggleFilePicker() {
|
||||
// INFO: Reset root dirs if home dir is selected
|
||||
if (homeDirSelected.value) {
|
||||
rootDirs.value = []
|
||||
}
|
||||
|
||||
showFilePicker.value = !showFilePicker.value
|
||||
}
|
||||
|
||||
function toggleHomeDir() {
|
||||
if (homeDirSelected.value) {
|
||||
rootDirs.value = []
|
||||
} else {
|
||||
rootDirs.value = [props.userhome]
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmitDirs(dirs: string[]) {
|
||||
rootDirs.value.push(...dirs)
|
||||
emit("error", "")
|
||||
showFilePicker.value = false
|
||||
}
|
||||
|
||||
function handleRemoveFolder(folder: string) {
|
||||
rootDirs.value = rootDirs.value.filter(dir => dir !== folder)
|
||||
}
|
||||
|
||||
function handleContinue() {
|
||||
if (!rootDirs.value.length) {
|
||||
emit('error', 'Please select a root directory')
|
||||
return
|
||||
}
|
||||
|
||||
emit('setRootDirs', rootDirs.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.rootdirconfig {
|
||||
width: 30rem;
|
||||
// text-align: center;
|
||||
|
||||
.heading {
|
||||
margin-bottom: $smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 1rem;
|
||||
border-radius: $small;
|
||||
background-color: $gray5;
|
||||
cursor: pointer;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray3;
|
||||
}
|
||||
|
||||
.option-selected {
|
||||
// width: 1.25rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: $gray1;
|
||||
|
||||
svg {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $small;
|
||||
}
|
||||
}
|
||||
|
||||
.userhome {
|
||||
font-weight: 500;
|
||||
font-family: 'SF Mono';
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: $smaller;
|
||||
}
|
||||
|
||||
.option-description {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $small;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selected-folders {
|
||||
position: absolute;
|
||||
top: 34rem;
|
||||
width: 100%;
|
||||
|
||||
background-color: $gray;
|
||||
padding: $small;
|
||||
padding-top: 1rem;
|
||||
|
||||
.folders {
|
||||
height: auto;
|
||||
max-height: 12rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.heading {
|
||||
position: absolute;
|
||||
top: -2rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: $gray1;
|
||||
}
|
||||
|
||||
// font-size: 0.8rem;
|
||||
|
||||
.action {
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/components/Onboarding/Welcome.vue
Normal file
52
src/components/Onboarding/Welcome.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<div class="logo"><LogoSvg /></div>
|
||||
<div class="heading">Welcome to</div>
|
||||
<div class="appname">Swing Music</div>
|
||||
<p class="tagline">
|
||||
You will need to configure your account login details <br />
|
||||
and root directories to get started.
|
||||
</p>
|
||||
<button class="btn-continue" @click="emit('continue')">Get Started</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LogoSvg from '@/assets/icons/logos/logo-fill.light.svg'
|
||||
|
||||
const emit = defineEmits(['continue'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $medium;
|
||||
|
||||
// center everything
|
||||
text-align: center;
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.appname {
|
||||
color: $highlight-blue;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
// gradient text
|
||||
background: linear-gradient(to right, $red, $blue, $red);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
// disable selection
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<form class="secretinput" @submit.prevent="$emit('submit', input)">
|
||||
<div class="left rounded-sm no-scroll">
|
||||
<input :type="showText ? 'text' : 'password'" v-model="input" @input="() => (showTextManual = true)" />
|
||||
<input v-model="input" :type="showText ? 'text' : 'password'" @input="() => (showTextManual = true)" />
|
||||
<button @click.prevent="showTextManual = !showTextManual">
|
||||
<EyeSvg v-if="showText" />
|
||||
<EyeSlashSvg v-else />
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
<div class="passinput">
|
||||
<input
|
||||
:id="props.inputId"
|
||||
v-model="value"
|
||||
class="passinput"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
@input="$emit('input', ($event.target as HTMLInputElement).value)"
|
||||
v-model="value"
|
||||
disabled
|
||||
/>
|
||||
<div
|
||||
class="showpass rounded-sm"
|
||||
v-if="props.type === 'password'"
|
||||
class="showpass rounded-sm"
|
||||
:class="{ show: value.length }"
|
||||
@click="toggleShowPassword"
|
||||
>
|
||||
@@ -30,6 +32,8 @@ const props = defineProps<{
|
||||
type?: string
|
||||
placeholder?: string
|
||||
inputId?: string
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const value = ref('')
|
||||
|
||||
@@ -28,6 +28,7 @@ const StatsView = () => import('@/views/Stats/main.vue')
|
||||
const MixView = () => import('@/views/MixView.vue')
|
||||
const MixListView = () => import('@/views/MixListView.vue')
|
||||
const Collection = () => import('@/views/Collections/Collection.vue')
|
||||
const Onboarding = () => import('@/views/Onboarding.vue')
|
||||
|
||||
const folder = {
|
||||
path: '/folder/:path',
|
||||
@@ -208,6 +209,12 @@ const PageView = {
|
||||
component: Collection,
|
||||
}
|
||||
|
||||
const OnboardingView = {
|
||||
path: '/onboarding',
|
||||
name: 'Onboarding',
|
||||
component: Onboarding,
|
||||
}
|
||||
|
||||
const routes = [
|
||||
folder,
|
||||
playlists,
|
||||
@@ -232,6 +239,7 @@ const routes = [
|
||||
Mix,
|
||||
MixList,
|
||||
PageView,
|
||||
OnboardingView,
|
||||
]
|
||||
|
||||
const Routes = {
|
||||
@@ -258,6 +266,7 @@ const Routes = {
|
||||
Mix: Mix.name,
|
||||
MixList: MixList.name,
|
||||
Page: PageView.name,
|
||||
Onboarding: OnboardingView.name,
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
134
src/views/Onboarding.vue
Normal file
134
src/views/Onboarding.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="onboarding pad-lg rounded-sm">
|
||||
<component
|
||||
:is="steps[currentStepIndex].component"
|
||||
v-bind="steps[currentStepIndex].props ?? {}"
|
||||
@continue="handleContinue"
|
||||
@account-created="handleAccountCreated"
|
||||
@set-root-dirs="handleRootDirs"
|
||||
@error="handleError"
|
||||
/>
|
||||
|
||||
<div class="progressbar">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="step.name"
|
||||
class="dot"
|
||||
:class="{ active: index === currentStepIndex, complete: index < currentStepIndex }"
|
||||
>
|
||||
<div class="dot-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errorText" class="errorbox rounded-md">
|
||||
<ErrorSvg />
|
||||
<div>{{ errorText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import ErrorSvg from '@/assets/icons/toast/error.svg'
|
||||
import Welcome from '@/components/Onboarding/Welcome.vue'
|
||||
import Account from '@/components/Onboarding/Account.vue'
|
||||
import RootDirs from '@/components/Onboarding/RootDirs.vue'
|
||||
|
||||
const errorText = ref('')
|
||||
const currentStepIndex = ref(2)
|
||||
const userhome = ref('/Users/cwilvx')
|
||||
|
||||
const steps = [
|
||||
{ name: 'welcome', component: Welcome },
|
||||
{ name: 'account', component: Account },
|
||||
{ name: 'dirconfig', component: RootDirs, props: { userhome: userhome.value } },
|
||||
{ name: 'finish' },
|
||||
]
|
||||
|
||||
function handleAccountCreated(user_home_path: string) {
|
||||
userhome.value = user_home_path
|
||||
currentStepIndex.value++
|
||||
}
|
||||
|
||||
function handleRootDirs(dirs: string[]) {
|
||||
console.log(dirs)
|
||||
// rootDirs.value = dirs
|
||||
}
|
||||
|
||||
function handleContinue() {
|
||||
errorText.value = ''
|
||||
currentStepIndex.value++
|
||||
}
|
||||
|
||||
function handleError(error: string) {
|
||||
errorText.value = error
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.onboarding {
|
||||
background-color: $gray;
|
||||
// border: solid 1px $gray4;
|
||||
|
||||
height: 30rem;
|
||||
width: 50rem;
|
||||
position: absolute;
|
||||
top: 25rem;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
position: relative;
|
||||
|
||||
// disable selection
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
||||
.btn-continue {
|
||||
background-color: $blue;
|
||||
border-radius: 4rem;
|
||||
padding: 1.25rem 2rem;
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
display: flex;
|
||||
gap: $small;
|
||||
position: absolute;
|
||||
bottom: -2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
background-color: $gray;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background-color: $gray3;
|
||||
}
|
||||
}
|
||||
|
||||
.errorbox {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 33.5rem;
|
||||
min-height: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: $red;
|
||||
background-color: $gray;
|
||||
padding: 1rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2rem 1fr;
|
||||
gap: $small;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,9 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||
const path = require("path");
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
|
||||
},
|
||||
base: "./",
|
||||
plugins: [
|
||||
vue(),
|
||||
@@ -97,6 +100,8 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
util: "util/",
|
||||
stream: "stream-browserify",
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user