kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
rodzic
df32d822e9
commit
8cbee81149
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Track, Album, Artist, Library, ArtistCredit } from '~/types'
|
import type { Track, Artist, Library, ArtistCredit } from '~/types'
|
||||||
|
|
||||||
import { momentFormat } from '~/utils/filters'
|
import { momentFormat } from '~/utils/filters'
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
@ -7,6 +7,7 @@ import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { sum } from 'lodash-es'
|
import { sum } from 'lodash-es'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
import { useDataStore } from '~/ui/stores/data'
|
||||||
import { useQueue } from '~/composables/audio/queue'
|
import { useQueue } from '~/composables/audio/queue'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
@ -35,13 +36,15 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const dataStore = useDataStore()
|
||||||
|
|
||||||
const emit = defineEmits<Events>()
|
const emit = defineEmits<Events>()
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const object = ref<Album | null>(null)
|
const object = computed(() => dataStore.get("album", props.id).value)
|
||||||
const artist = ref<Artist | null>(null)
|
const artistCredit = computed(() => object.value?.artist_credit ?? [])
|
||||||
const artistCredit = ref([] as ArtistCredit[])
|
const artist = computed(() => dataStore.get("artist", artistCredit.value[0].artist.id))
|
||||||
|
|
||||||
const libraries = ref([] as Library[])
|
const libraries = ref([] as Library[])
|
||||||
const paginateBy = ref(50)
|
const paginateBy = ref(50)
|
||||||
|
|
||||||
|
@ -75,20 +78,10 @@ const {
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
if (!object.value)
|
||||||
const albumResponse = await axios.get(`albums/${props.id}/`, { params: { refresh: 'true' } })
|
return
|
||||||
|
else
|
||||||
artistCredit.value = albumResponse.data.artist_credit
|
|
||||||
|
|
||||||
// fetch the first artist of the album
|
|
||||||
const artistResponse = await axios.get(`artists/${albumResponse.data.artist_credit[0].artist.id}/`)
|
|
||||||
|
|
||||||
artist.value = artistResponse.data
|
|
||||||
|
|
||||||
object.value = albumResponse.data
|
|
||||||
if (object.value) {
|
|
||||||
object.value.tracks = []
|
object.value.tracks = []
|
||||||
}
|
|
||||||
|
|
||||||
fetchTracks()
|
fetchTracks()
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
@ -128,7 +121,7 @@ const fetchTracks = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.id, fetchData, { immediate: true })
|
watch(() => [props.id, object.value], fetchData, { immediate: true })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
@ -154,6 +147,7 @@ const remove = async () => {
|
||||||
/>
|
/>
|
||||||
<Header
|
<Header
|
||||||
v-if="object"
|
v-if="object"
|
||||||
|
:key="object.title /*Re-render component when title changes after an update*/"
|
||||||
:h1="object.title"
|
:h1="object.title"
|
||||||
page-heading
|
page-heading
|
||||||
>
|
>
|
||||||
|
@ -272,7 +266,7 @@ const remove = async () => {
|
||||||
</Layout>
|
</Layout>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<div style="flex 1;">
|
<div style="flex: 1;">
|
||||||
<router-view
|
<router-view
|
||||||
v-if="object"
|
v-if="object"
|
||||||
:key="route.fullPath"
|
:key="route.fullPath"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { isEqual, clone } from 'lodash-es'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useDataStore } from '~/ui/stores/data'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
@ -34,14 +35,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
licenses: () => []
|
licenses: () => []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Since the object may be reactive (self-updating), we need a clone to compare changes
|
||||||
|
const originalObject = Object.assign({}, props.object)
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const configs = useEditConfigs()
|
const configs = useEditConfigs()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const dataStore = useDataStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const config = computed(() => configs[props.objectType])
|
const config = computed(() => configs[props.objectType])
|
||||||
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
|
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
|
||||||
state[field.id] = { value: field.getValue(props.object) }
|
state[field.id] = { value: field.getValue(originalObject) }
|
||||||
return state
|
return state
|
||||||
}, {}))
|
}, {}))
|
||||||
|
|
||||||
|
@ -123,6 +128,9 @@ const submit = async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
submittedMutation.value = response.data
|
submittedMutation.value = response.data
|
||||||
|
|
||||||
|
// Immediately re-fetch the updated object into the store
|
||||||
|
dataStore.get(props.objectType, props.object.id!.toString(), { immediate: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.value = (error as BackendError).backendErrors
|
errors.value = (error as BackendError).backendErrors
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed, type Ref } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import type { Album, Artist, Track } from '~/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch an item from the API.
|
||||||
|
* - Rate limiting: Caches the result for 1 second to prevent over-fetching and request duplication (override with { immediate: true})
|
||||||
|
* - Sharing reactive objects across components: Avoid data duplication, and auto-update the Ui whenever an updated version of the data is re-fetched
|
||||||
|
* - Strongly typed results
|
||||||
|
* - TODO: Errors and Loading states (`undefined` can mean an error occurred, or the request is still pending)
|
||||||
|
*
|
||||||
|
* **Example**
|
||||||
|
* ```ts
|
||||||
|
* import { useDataStore } from '~/ui/stores/data'
|
||||||
|
*
|
||||||
|
* const data = useDataStore()
|
||||||
|
*
|
||||||
|
* artist15 = data.get("artist", "15") // Ref<Artist | undefined>
|
||||||
|
* const album23 = data.get("album", "23") // Ref<Album | undefined>
|
||||||
|
* ```
|
||||||
|
* As soon as you re-fetch data, all references to the same object in all components using this store will be updated.
|
||||||
|
*
|
||||||
|
* Note: Pinia does not support destructuring.
|
||||||
|
* Do not write `{ get } = useDataStore()`
|
||||||
|
*/
|
||||||
|
export const useDataStore
|
||||||
|
= defineStore('data', () => {
|
||||||
|
|
||||||
|
// Type map that associates cache keys with their corresponding types
|
||||||
|
type ItemType = {
|
||||||
|
artist: Artist
|
||||||
|
album: Album
|
||||||
|
track: Track
|
||||||
|
// Add new types here (channel: Channel...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Items<I extends keyof ItemType> = Record<number | string, {
|
||||||
|
result: Ref<ItemType[I] | undefined>;
|
||||||
|
timestamp: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type Cache = {
|
||||||
|
[I in keyof ItemType]: Items<I>
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache: Cache = {
|
||||||
|
artist: {},
|
||||||
|
album: {},
|
||||||
|
track: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inspect the cache with the Vue Devtools (Pinia tab); read-only */
|
||||||
|
const data = computed(() => cache)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param type - either 'artist' or 'album' etc.
|
||||||
|
* @param id - The ID of the item to fetch.
|
||||||
|
* @param immediate - Whether to re-fetch immediately (default: only re-fetch data older than 1 second)
|
||||||
|
* @returns an auto-updating reference to `undefined` if there is an error or the item is not yet loaded, or the actual item once it is loaded.
|
||||||
|
*
|
||||||
|
* Tip: Re-run after 1 second to refresh the data.
|
||||||
|
*/
|
||||||
|
const get = <I extends keyof ItemType>(type: I, id: number | string, options?: { immediate?: boolean }) => {
|
||||||
|
// Override limited typescript inference (Remove assertion once typescript can infer correctly)
|
||||||
|
const items = cache[type] as Items<I>
|
||||||
|
|
||||||
|
// Initialize the object if it doesn't exist
|
||||||
|
if (!items[id])
|
||||||
|
items[id] = { result: ref(undefined) as Ref<ItemType[I] | undefined>, timestamp: 0 }
|
||||||
|
|
||||||
|
// Re-fetch if immediate is true or the item is not cached or older than 1 second
|
||||||
|
if (options?.immediate || items[id].timestamp < Date.now() - 1000)
|
||||||
|
axios.get<ItemType[I]>(`${type}s/${id}/`, { params: { refresh: 'true' } }).then(({ data }) => {
|
||||||
|
items[id].result.value = data;
|
||||||
|
items[id].timestamp = Date.now();
|
||||||
|
})
|
||||||
|
return items[id].result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
get
|
||||||
|
}
|
||||||
|
})
|
Ładowanie…
Reference in New Issue