feat: add cloud encyclopedia system
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import type { CloudType } from '@/types/database'
|
||||
|
||||
export interface UploadItem {
|
||||
id: string
|
||||
@@ -16,6 +17,19 @@ export interface UploadItem {
|
||||
errors: Record<string, string>
|
||||
}
|
||||
|
||||
export interface UnlockedBadge {
|
||||
cloudTypeId: number
|
||||
cloudName: string
|
||||
cloudNameEn: string
|
||||
rarity: CloudType['rarity']
|
||||
unlockedAt: string
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
ok: boolean
|
||||
unlockedBadges: UnlockedBadge[]
|
||||
}
|
||||
|
||||
let nextId = 0
|
||||
|
||||
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
@@ -75,6 +89,50 @@ function extractExifDate(buffer: ArrayBuffer): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
async function fetchUnlockedTypeIds(userId: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('user_collections')
|
||||
.select('cloud_type_id')
|
||||
.eq('user_id', userId)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return new Set(
|
||||
(data || [])
|
||||
.map(row => row.cloud_type_id)
|
||||
.filter((cloudTypeId): cloudTypeId is number => typeof cloudTypeId === 'number'),
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchBadgeDetails(unlockedRows: Array<{ cloudTypeId: number; unlockedAt: string }>) {
|
||||
const ids = unlockedRows.map(item => item.cloudTypeId)
|
||||
const { data, error } = await supabase
|
||||
.from('cloud_types')
|
||||
.select('id,name,name_en,rarity')
|
||||
.in('id', ids)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
const typeMap = new Map(
|
||||
((data || []) as Array<Pick<CloudType, 'id' | 'name' | 'name_en' | 'rarity'>>).map(item => [item.id, item]),
|
||||
)
|
||||
|
||||
return unlockedRows
|
||||
.map(item => {
|
||||
const cloudType = typeMap.get(item.cloudTypeId)
|
||||
if (!cloudType) return null
|
||||
|
||||
return {
|
||||
cloudTypeId: cloudType.id,
|
||||
cloudName: cloudType.name,
|
||||
cloudNameEn: cloudType.name_en,
|
||||
rarity: cloudType.rarity,
|
||||
unlockedAt: item.unlockedAt,
|
||||
} satisfies UnlockedBadge
|
||||
})
|
||||
.filter((item): item is UnlockedBadge => item !== null)
|
||||
}
|
||||
|
||||
export function useUpload() {
|
||||
const items = ref<UploadItem[]>([])
|
||||
const uploading = ref(false)
|
||||
@@ -163,8 +221,10 @@ export function useUpload() {
|
||||
return Math.round(value * 100) / 100
|
||||
}
|
||||
|
||||
async function uploadAll(): Promise<boolean> {
|
||||
if (!validateAll()) return false
|
||||
async function uploadAll(): Promise<UploadResult> {
|
||||
if (!validateAll()) {
|
||||
return { ok: false, unlockedBadges: [] }
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
totalItems.value = items.value.length
|
||||
@@ -174,10 +234,19 @@ export function useUpload() {
|
||||
const userId = (await supabase.auth.getUser()).data.user?.id
|
||||
if (!userId) {
|
||||
uploading.value = false
|
||||
return false
|
||||
return { ok: false, unlockedBadges: [] }
|
||||
}
|
||||
|
||||
try {
|
||||
let unlockedTypeIds = new Set<number>()
|
||||
try {
|
||||
unlockedTypeIds = await fetchUnlockedTypeIds(userId)
|
||||
} catch {
|
||||
unlockedTypeIds = new Set<number>()
|
||||
}
|
||||
|
||||
const newlyUnlockedRows: Array<{ cloudTypeId: number; unlockedAt: string }> = []
|
||||
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const item = items.value[i]
|
||||
currentItemIndex.value = i + 1
|
||||
@@ -200,7 +269,7 @@ export function useUpload() {
|
||||
const latitude = item.latitude ? blurCoordinate(item.latitude) : null
|
||||
const longitude = item.longitude ? blurCoordinate(item.longitude) : null
|
||||
|
||||
const { error: dbError } = await supabase
|
||||
const { data: insertedCloud, error: dbError } = await supabase
|
||||
.from('clouds')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
@@ -216,8 +285,41 @@ export function useUpload() {
|
||||
status: 'approved',
|
||||
is_hidden: item.isHidden,
|
||||
})
|
||||
.select('id,cloud_type_id')
|
||||
.single()
|
||||
if (dbError) throw dbError
|
||||
|
||||
if (
|
||||
insertedCloud &&
|
||||
typeof insertedCloud.cloud_type_id === 'number' &&
|
||||
!unlockedTypeIds.has(insertedCloud.cloud_type_id)
|
||||
) {
|
||||
try {
|
||||
const { data: collectionRow, error: collectionError } = await supabase
|
||||
.from('user_collections')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
cloud_type_id: insertedCloud.cloud_type_id,
|
||||
first_cloud_id: insertedCloud.id,
|
||||
})
|
||||
.select('cloud_type_id,unlocked_at')
|
||||
.single()
|
||||
|
||||
if (collectionError) {
|
||||
const duplicate = collectionError.code === '23505'
|
||||
if (!duplicate) throw collectionError
|
||||
} else if (collectionRow) {
|
||||
unlockedTypeIds.add(collectionRow.cloud_type_id)
|
||||
newlyUnlockedRows.push({
|
||||
cloudTypeId: collectionRow.cloud_type_id,
|
||||
unlockedAt: collectionRow.unlocked_at,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Ignore collection sync failures so uploads can still complete.
|
||||
}
|
||||
}
|
||||
|
||||
overallProgress.value = Math.round((i + 1) / items.value.length * 100)
|
||||
}
|
||||
|
||||
@@ -225,9 +327,12 @@ export function useUpload() {
|
||||
URL.revokeObjectURL(item.preview)
|
||||
}
|
||||
items.value = []
|
||||
return true
|
||||
return {
|
||||
ok: true,
|
||||
unlockedBadges: newlyUnlockedRows.length ? await fetchBadgeDetails(newlyUnlockedRows) : [],
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
return { ok: false, unlockedBadges: [] }
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user