Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add song editor and validator #84

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 7 additions & 45 deletions app/components/editors/SongEditor.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { MaruSongDataParsed } from '@marure/schema'
import { useValidation } from '@/composables/validator'
import { parseLrc, secondsToTimestamp, serializeToLrc } from '@marure/parser'
import { inferSongInfoFromVideoTitle } from '@marure/utils'
import { useDebouncedRefHistory } from '@vueuse/core'
Expand Down Expand Up @@ -37,6 +38,8 @@ const {
const router = useRouter()
const route = useRoute()

const { validate, errors } = useValidation(stateRef.value)

const dirty = ref(false)

watch(
Expand Down Expand Up @@ -64,6 +67,9 @@ async function save() {
alert(t('youtube.requireId'))
return
}
if (!validate()) {
return
}
syncLrc()
const copy = { ...toRaw(stateRef.value), lyrics: undefined }
await saveSongsToLocal([copy])
Expand Down Expand Up @@ -228,34 +234,6 @@ function toggleTranslations(lang: string) {
}
}

const artistsString = computed({
get: () => (stateRef.value.artists || []).join(', '),
set: (value: string) => {
stateRef.value.artists = value.split(',').map(v => v.trim())
},
})

const tagsString = computed({
get: () => (stateRef.value.tags || []).join(', '),
set: (value: string) => {
stateRef.value.tags = value.split(',').map(v => v.trim())
},
})

const offsetString = computed({
get: () => String(stateRef.value.offset || ''),
set: (value: string) => {
stateRef.value.offset = Number(value)
},
})

const notesString = computed({
get: () => (stateRef.value.notes || []).join('\n'),
set: (value: string) => {
stateRef.value.notes = value.split('\n')
},
})

const { copied, copy } = useClipboard({ read: false })

const currentTimestamp = computed(() => secondsToTimestamp(controls.current.value - (stateRef.value.offset ?? 0)))
Expand Down Expand Up @@ -341,23 +319,7 @@ onMounted(() => {
</DraggableWindow>
<div mxa max-w-300 flex="~ col gap-3">
<BasicNav />
<div flex="~ col gap-2" max-w-150>
<h1 my4 text-2xl>
{{ $t("lyrics.editLyrics") }}
</h1>
<TextInput :model-value="stateRef.youtube" label="YouTube ID" input-class="font-mono" disabled />
<TextInput v-model="stateRef.title" :label="$t('song.title')" />
<TextInput v-model="artistsString" :label="$t('song.artist')" />
<TextInput v-model="tagsString" :label="$t('song.tags')" />
<TextInput v-model="offsetString" :label="$t('song.offset')" />
<TextInput
v-model="notesString"
:label="$t('common.notes')"
type="textarea"
input-class="h-30"
/>
</div>

<SongForm :state-ref="state" :errors="errors" />
<div flex="~ gap-2 items-center" mt5>
<SimpleButton
:class="showTab === 'lyrics' ? '' : 'op50'"
Expand Down
68 changes: 68 additions & 0 deletions app/components/editors/SongForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
import type { MaruSongDataParsed } from '@marure/schema'
import type { Reactive } from 'vue'

const props = defineProps<{
stateRef: Reactive<MaruSongDataParsed>
errors: Reactive<ValidationErrors>
}>()

const stateRef = toRef(props.stateRef)
const { title, artists, tags } = toRefs(props.errors)

const artistsString = computed({
get: () => (stateRef.value.artists || []).join(', '),
set: (value: string) => {
stateRef.value.artists = value.split(',').map(v => v.trim())
},
})

const tagsString = computed({
get: () => (stateRef.value.tags || []).join(', '),
set: (value: string) => {
stateRef.value.tags = value.split(',').map(v => v.trim())
},
})

const offsetString = computed({
get: () => String(stateRef.value.offset || ''),
set: (value: string) => {
stateRef.value.offset = Number(value)
},
})

const notesString = computed({
get: () => (stateRef.value.notes || []).join('\n'),
set: (value: string) => {
stateRef.value.notes = value.split('\n')
},
})
</script>

<template>
<div flex="~ col gap-2" max-w-150>
<h1 my4 text-2xl>
{{ $t("lyrics.editLyrics") }}
</h1>
<TextInput :model-value="stateRef.youtube" label="YouTube ID" input-class="font-mono" disabled />
<div>
<TextInput v-model="stateRef.title" :label="$t('song.title')" />
<span class="text-sm text-red-500">{{ title }}</span>
</div>
<div>
<TextInput v-model="artistsString" :label="$t('song.artist')" />
<span v-if="artists" class="text-sm text-red-500">{{ artists }}</span>
</div>
<div>
<TextInput v-model="tagsString" :label="$t('song.tags')" />
<span v-if="tags" class="text-sm text-red-500">{{ tags }}</span>
</div>
<TextInput v-model="offsetString" :label="$t('song.offset')" />
<TextInput
v-model="notesString"
:label="$t('common.notes')"
type="textarea"
input-class="h-30"
/>
</div>
</template>
57 changes: 57 additions & 0 deletions app/composables/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { MaruSongDataParsed } from '@marure/schema'
import { useNuxtApp } from 'nuxt/app'
import { reactive } from 'vue'

export interface ValidationErrors {
youtube: string
title: string
artists: string
tags: string
offset?: string
notes?: string
}

export function useValidation(stateRef: MaruSongDataParsed) {
const errors = reactive<ValidationErrors>({
youtube: '',
title: '',
artists: '',
tags: '',
})
const nuxtApp = useNuxtApp()
const { t } = nuxtApp.$i18n

const validate = (): boolean => {
let isValid = true

errors.youtube = ''
errors.title = ''
errors.artists = ''
errors.tags = ''

if (!stateRef.youtube || !stateRef.youtube.trim()) {
errors.youtube = t('editor.validator.youtube')
isValid = false
}

if (!stateRef.title || !stateRef.title.trim()) {
errors.title = t('editor.validator.title')
isValid = false
}
if (!stateRef.artists || stateRef.artists?.includes('')) {
errors.artists = t('editor.validator.artists')
isValid = false
}

if (!stateRef.tags || stateRef.tags?.includes('')) {
errors.tags = t('editor.validator.tags')
isValid = false
}
return isValid
}

return {
errors,
validate,
}
}
8 changes: 7 additions & 1 deletion app/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@
"setCurrentTime": "Apply current playback time and go next",
"setCurrentTimeOnly": "Apply current playback time"
},
"visualization": "Visualization"
"visualization": "Visualization",
"validator": {
"youtube": "YouTube ID is required.",
"title": "Title is required.",
"artists": "At least one artist is required.",
"tags": "At least one tag is required."
}
},
"footer": {
"fileFormat": "File Format",
Expand Down
8 changes: 7 additions & 1 deletion app/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@
"setCurrentTime": "現在の再生時間に設定して次の行へ",
"setCurrentTimeOnly": "現在の再生時間に設定"
},
"visualization": "可視化"
"visualization": "可視化",
"validator": {
"youtube": "YouTube IDは必須です。",
"title": "タイトルは必須です。",
"artists": "少なくとも1人のアーティストが必要です。",
"tags": "少なくとも1つのタグが必要です。"
}
},
"footer": {
"fileFormat": "ファイル形式",
Expand Down
9 changes: 8 additions & 1 deletion app/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@
"setCurrentTime": "设为当前播放时间后跳行",
"setCurrentTimeOnly": "设为当前播放时间"
},
"visualization": "可视化"
"visualization": "可视化",
"validator": {
"youtube": "YouTube ID 是必填的。",
"title": "标题是必填的。",
"artists": "至少需要一个歌手。",
"tags": "至少需要一个标签。"
}

},
"footer": {
"fileFormat": "文件格式",
Expand Down
8 changes: 7 additions & 1 deletion app/locales/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@
"setCurrentTime": "設為當前播放時間後跳行",
"setCurrentTimeOnly": "設為當前播放時間"
},
"visualization": "可視化"
"visualization": "可視化",
"validator": {
"youtube": "YouTube ID 是必填的。",
"title": "標題是必填的。",
"artists": "至少需要一個歌手。",
"tags": "至少需要一個標籤。"
}
},
"footer": {
"fileFormat": "檔案格式",
Expand Down
Loading