mirror of
https://github.com/tranxuanthang/lrcget.git
synced 2026-04-17 21:47:04 -04:00
Add conflict modal and partially working LRCLIB edit lyrics flow
This commit is contained in:
@@ -396,9 +396,11 @@ Users can pick:
|
||||
- [x] Phase 4: LRCLIB lyricsfile integration
|
||||
- [x] Migration 12: Add `lrclib_instance` and `lrclib_id` columns to `lyricsfiles` table
|
||||
- [x] Add `prepare_lrclib_lyricsfile` command to fetch/create standalone lyricsfiles
|
||||
- [x] Add `refresh_lrclib_lyricsfile` command to force re-download from LRCLIB
|
||||
- [x] Update `save_lyrics` command to support standalone lyricsfiles
|
||||
- [x] Update `AssociateTrackModal.vue` - remove "Edit without audio" option
|
||||
- [x] Update `SearchResult.vue` - use `prepare_lrclib_lyricsfile` command
|
||||
- [x] Create `LyricsfileConflictModal.vue` - ask user to redownload or continue when lyrics already exist
|
||||
- [x] Update `EditLyricsV2.vue` - accept `lyricsfileId` and `initialLyricsfile` props
|
||||
- [x] Update `useEditLyricsV2Document.js` - support initialization from external lyricsfile
|
||||
- [ ] Phase 5: Remove legacy `EditLyrics.vue` (after testing)
|
||||
|
||||
@@ -201,7 +201,8 @@ Implements `From<PersistentTrack>` for seamless conversion from database entitie
|
||||
| `retrieve_lyrics/by_id()` | Get raw LRCLIB response |
|
||||
| `search_lyrics()` | Search LRCLIB database |
|
||||
| `apply_lyrics()` | Save a selected LRCLIB result into database-backed lyrics storage |
|
||||
| `prepare_lrclib_lyricsfile(lrclib_id)` | Get or create lyricsfile from LRCLIB. Checks local cache first, fetches from API if needed. Saves to `lyricsfiles` table with `lrclib_instance` + `lrclib_id`. Returns `lyricsfile_id` + content. |
|
||||
| `prepare_lrclib_lyricsfile(lrclib_id)` | Get or create lyricsfile from LRCLIB. Checks local cache first, fetches from API if needed. Saves to `lyricsfiles` table with `lrclib_instance` + `lrclib_id`. Returns `lyricsfile_id` + content + `exists_in_db` flag. |
|
||||
| `refresh_lrclib_lyricsfile(lrclib_id)` | Force re-download lyrics from LRCLIB API. Updates existing record in `lyricsfiles` table. Returns refreshed `lyricsfile_id` + content. |
|
||||
| `save_lyrics(track_id?, lyricsfile_id?, plain?, synced?, lyricsfile?)` | Save lyrics edits. For library tracks: provide `track_id`. For standalone LRCLIB lyrics: provide `lyricsfile_id`. Prefers `lyricsfile` format. |
|
||||
| `publish_lyrics(title, album, artist, duration, plain?, synced?, lyricsfile?)` | Upload to LRCLIB (with PoW; accepts Lyricsfile-only payloads) |
|
||||
| `export_lyrics(track_id, formats, lyricsfile?)` | Manual export to `.txt`, `.lrc`, or embedded tags |
|
||||
|
||||
@@ -796,6 +796,7 @@ struct PrepareLyricsfileResult {
|
||||
pub plain_lyrics: String,
|
||||
pub synced_lyrics: String,
|
||||
pub is_instrumental: bool,
|
||||
pub exists_in_db: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -826,6 +827,7 @@ async fn prepare_lrclib_lyricsfile(
|
||||
plain_lyrics: parsed.plain_lyrics.unwrap_or_default(),
|
||||
synced_lyrics: parsed.synced_lyrics.unwrap_or_default(),
|
||||
is_instrumental: parsed.is_instrumental,
|
||||
exists_in_db: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -881,6 +883,75 @@ async fn prepare_lrclib_lyricsfile(
|
||||
plain_lyrics: parsed.plain_lyrics.unwrap_or_default(),
|
||||
synced_lyrics: parsed.synced_lyrics.unwrap_or_default(),
|
||||
is_instrumental: parsed.is_instrumental,
|
||||
exists_in_db: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh_lrclib_lyricsfile(
|
||||
lrclib_id: i64,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<PrepareLyricsfileResult, String> {
|
||||
// Get config for LRCLIB instance URL
|
||||
let config = app_handle
|
||||
.db(|db: &Connection| db::get_config(db))
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let lrclib_instance = config.lrclib_instance;
|
||||
|
||||
// Fetch fresh data from LRCLIB API (always re-download)
|
||||
let lrclib_response = lrclib::get_by_id::request_raw(lrclib_id, &lrclib_instance)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
// Extract metadata from LRCLIB response
|
||||
let title = lrclib_response.name.unwrap_or_default();
|
||||
let album_name = lrclib_response.album_name.unwrap_or_default();
|
||||
let artist_name = lrclib_response.artist_name.unwrap_or_default();
|
||||
let duration = lrclib_response.duration.unwrap_or(0.0);
|
||||
|
||||
// Build or use existing lyricsfile content
|
||||
let lyricsfile_content = if let Some(lyricsfile) = lrclib_response.lyricsfile {
|
||||
// LRCLIB provided a lyricsfile, use it directly
|
||||
lyricsfile
|
||||
} else {
|
||||
// Need to build lyricsfile from plain/synced lyrics
|
||||
let metadata =
|
||||
lyricsfile::LyricsfileTrackMetadata::new(&title, &album_name, &artist_name, duration);
|
||||
|
||||
let plain = lrclib_response.plain_lyrics.as_deref();
|
||||
let synced = lrclib_response.synced_lyrics.as_deref();
|
||||
|
||||
lyricsfile::build_lyricsfile(&metadata, plain, synced)
|
||||
.ok_or("Failed to build lyricsfile from LRCLIB response")?
|
||||
};
|
||||
|
||||
// Parse for return values
|
||||
let parsed = lyricsfile::parse_lyricsfile(&lyricsfile_content).map_err(|e| e.to_string())?;
|
||||
|
||||
// Save to database (will update existing if lrclib_instance + lrclib_id match)
|
||||
let lyricsfile_id = app_handle
|
||||
.db(|db: &Connection| {
|
||||
db::upsert_lyricsfile_for_lrclib(
|
||||
&lrclib_instance,
|
||||
lrclib_id,
|
||||
&title,
|
||||
&album_name,
|
||||
&artist_name,
|
||||
duration,
|
||||
&lyricsfile_content,
|
||||
db,
|
||||
)
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(PrepareLyricsfileResult {
|
||||
lyricsfile_id,
|
||||
lyricsfile: lyricsfile_content,
|
||||
plain_lyrics: parsed.plain_lyrics.unwrap_or_default(),
|
||||
synced_lyrics: parsed.synced_lyrics.unwrap_or_default(),
|
||||
is_instrumental: parsed.is_instrumental,
|
||||
exists_in_db: true, // After refresh, it definitely exists
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1552,6 +1623,7 @@ async fn main() {
|
||||
get_audio_metadata,
|
||||
prepare_search_query,
|
||||
prepare_lrclib_lyricsfile,
|
||||
refresh_lrclib_lyricsfile,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -79,7 +79,7 @@ Module-level ref composables (singletons by design):
|
||||
- _V2 Instrumental Support_: Tracks can be marked as instrumental via `PlainLyricsEmptyState.vue` or `SyncedLyricsEmptyState.vue`. When marked, the plain/synced tab switcher is disabled and a centered popup appears with an "Unmark as instrumental" button. The instrumental state is stored in the `lyricsfile` metadata and managed by `useEditLyricsV2Document.js`.
|
||||
- **Export (Mass)**: `LibraryHeader.vue` has an export button (with dropdown) that emits `exportAllLyrics` → `Library.vue` opens `ExportViewer.vue` modal → `useExporter()` composable manages queue → invokes `export_track_lyrics` command per track. Exports to `.txt`, `.lrc`, and/or embedded metadata.
|
||||
- **My LRCLIB**: User workflows (preview, edit, publish, flag) in `my-lrclib/`
|
||||
- **Track Association (My LRCLIB Edit Flow)**: When editing lyrics from My LRCLIB search results, the flow is: (1) Call `prepare_lrclib_lyricsfile(lrclibId)` to fetch/create the lyricsfile in the database (stored with `lrclib_instance` and `lrclib_id`), (2) Show `AssociateTrackModal.vue` for selecting either a library track or a file from the computer, (3) Open `EditLyricsV2.vue` with the selected audio source and the standalone lyricsfile. The `get_audio_metadata` backend command extracts metadata from selected files using the existing scanner logic. Supports playback for both library tracks and arbitrary file-based tracks.
|
||||
- **Track Association (My LRCLIB Edit Flow)**: When editing lyrics from My LRCLIB search results, the flow is: (1) Call `prepare_lrclib_lyricsfile(lrclibId)` to fetch/create the lyricsfile in the database (stored with `lrclib_instance` and `lrclib_id`). If lyrics already exist locally, show `LyricsfileConflictModal.vue` to ask if user wants to redownload from LRCLIB or continue with local version. (2) Show `AssociateTrackModal.vue` for selecting either a library track or a file from the computer. (3) Open `EditLyricsV2.vue` with the selected audio source and the standalone lyricsfile. The `get_audio_metadata` backend command extracts metadata from selected files using the existing scanner logic. Supports playback for both library tracks and arbitrary file-based tracks.
|
||||
|
||||
Utils: `src/utils/` (parsing, linting), Composables: `composables/edit-lyrics/`, `composables/edit-lyrics-v2/`, `composables/export.js`
|
||||
|
||||
|
||||
@@ -139,18 +139,32 @@ import { useGlobalState } from '@/composables/global-state.js'
|
||||
import { usePlayer } from '@/composables/player.js'
|
||||
|
||||
const props = defineProps({
|
||||
track: {
|
||||
// Audio source for playback (library track or file-based track)
|
||||
audioSource: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
// Source type: 'track' for normal library editing, 'lrclib' for LRCLIB flow
|
||||
source: {
|
||||
type: String,
|
||||
default: 'track',
|
||||
validator: (value) => ['track', 'lrclib'].includes(value),
|
||||
},
|
||||
// For LRCLIB flow: standalone lyricsfile ID
|
||||
lyricsfileId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
initialLyricsfile: {
|
||||
// For LRCLIB flow: initial lyricsfile content
|
||||
lyricsfileContent: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
// For LRCLIB flow: lyricsfile metadata (title, artist, album, duration_ms)
|
||||
lyricsfileMetadata: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
76
src/components/library/my-lrclib/LyricsfileConflictModal.vue
Normal file
76
src/components/library/my-lrclib/LyricsfileConflictModal.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
title="Existing Lyrics Found"
|
||||
content-class="w-full max-w-md"
|
||||
:close-button="true"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div>Lyrics for this track were previously downloaded.</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="button button-normal px-4 py-2 rounded-full text-sm"
|
||||
:disabled="isLoading"
|
||||
@click="redownload"
|
||||
>
|
||||
<Loading v-if="isLoading" class="animate-spin mr-1" />
|
||||
<Refresh v-else class="mr-1" />
|
||||
Redownload
|
||||
</button>
|
||||
<button
|
||||
class="button button-primary px-4 py-2 rounded-full text-sm"
|
||||
@click="continueEditing"
|
||||
>
|
||||
<Pencil class="mr-1" />
|
||||
Continue Editing
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import Loading from '~icons/mdi/loading'
|
||||
import Refresh from '~icons/mdi/refresh'
|
||||
import Pencil from '~icons/mdi/pencil'
|
||||
|
||||
const props = defineProps({
|
||||
lrclibId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
existingResult: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'redownload', 'continue'])
|
||||
|
||||
const toast = useToast()
|
||||
const isLoading = ref(false)
|
||||
|
||||
const redownload = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const result = await invoke('refresh_lrclib_lyricsfile', { lrclibId: props.lrclibId })
|
||||
emit('redownload', result)
|
||||
emit('close')
|
||||
} catch (error) {
|
||||
console.error('Error refreshing lyrics:', error)
|
||||
toast.error('Failed to redownload lyrics from LRCLIB')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const continueEditing = () => {
|
||||
emit('continue', props.existingResult)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
@@ -116,6 +116,7 @@ import EditLyrics from './EditLyrics.vue'
|
||||
import PreviewLyrics from './PreviewLyrics.vue'
|
||||
import FlagLyrics from './FlagLyrics.vue'
|
||||
import AssociateTrackModal from './AssociateTrackModal.vue'
|
||||
import LyricsfileConflictModal from './LyricsfileConflictModal.vue'
|
||||
import EditLyricsV2 from '../EditLyricsV2.vue'
|
||||
import { useModal } from 'vue-final-modal'
|
||||
|
||||
@@ -225,6 +226,36 @@ const { open: openAssociateTrackModal, close: closeAssociateTrackModal } = useMo
|
||||
},
|
||||
})
|
||||
|
||||
// Conflict modal state
|
||||
const conflictModalLrclibId = ref(null)
|
||||
const conflictModalExistingResult = ref(null)
|
||||
|
||||
const { open: openConflictModal, close: closeConflictModal } = useModal({
|
||||
component: LyricsfileConflictModal,
|
||||
attrs: {
|
||||
lrclibId: conflictModalLrclibId,
|
||||
existingResult: conflictModalExistingResult,
|
||||
onClose() {
|
||||
closeConflictModal()
|
||||
},
|
||||
onClosed() {
|
||||
conflictModalLrclibId.value = null
|
||||
conflictModalExistingResult.value = null
|
||||
},
|
||||
onRedownload(result) {
|
||||
// User chose to redownload - proceed with refreshed data
|
||||
proceedToAssociateTrackModal(conflictModalLrclibTrack.value, result)
|
||||
},
|
||||
onContinue(result) {
|
||||
// User chose to continue with existing - proceed with existing data
|
||||
proceedToAssociateTrackModal(conflictModalLrclibTrack.value, result)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Store the track temporarily while showing conflict modal
|
||||
const conflictModalLrclibTrack = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
const config = await invoke('get_config')
|
||||
showLineCount.value = config.show_line_count
|
||||
@@ -259,17 +290,30 @@ const setShowingTrack = async track => {
|
||||
}
|
||||
}
|
||||
|
||||
const proceedToAssociateTrackModal = (track, result) => {
|
||||
// Set the data and open associate modal
|
||||
associateModalLrclibTrack.value = track
|
||||
associateModalLyricsfileId.value = result.lyricsfileId
|
||||
associateModalInitialLyricsfile.value = result.lyricsfile
|
||||
openAssociateTrackModal()
|
||||
}
|
||||
|
||||
const setEditingTrack = async track => {
|
||||
isOpeningTrack.value = true
|
||||
try {
|
||||
// Prepare lyricsfile from LRCLIB (fetches or gets from cache)
|
||||
const result = await invoke('prepare_lrclib_lyricsfile', { lrclibId: track.id })
|
||||
|
||||
// Show association modal
|
||||
associateModalLrclibTrack.value = track
|
||||
associateModalLyricsfileId.value = result.lyricsfileId
|
||||
associateModalInitialLyricsfile.value = result.lyricsfile
|
||||
openAssociateTrackModal()
|
||||
if (result.existsInDb) {
|
||||
// Lyrics already exist - show conflict modal
|
||||
conflictModalLrclibTrack.value = track
|
||||
conflictModalLrclibId.value = track.id
|
||||
conflictModalExistingResult.value = result
|
||||
openConflictModal()
|
||||
} else {
|
||||
// New lyrics - proceed directly to associate modal
|
||||
proceedToAssociateTrackModal(track, result)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('An error occurred while opening the lyrics. Please try again.')
|
||||
console.error(error)
|
||||
|
||||
Reference in New Issue
Block a user