feat: page, sort and search stored media and delete several at once in the admin API
This commit is contained in:
1 parent
c39331aa75
commit
fb751ca57f
23 files changed
+1161
-213
No files matched your search
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
import type { APIKey, Account, AdminAccount, ErrorInfo, GitHubClient, GitHubIntent, ImportedTrack, Member, MyListen, MyPlaylist, MyPlaylistItem, QQLoginType, QRLogin, QRLoginResult, RoomListing, SharedPlaylist, Source, SourceAccount, StoredMedia, Track } from '../realtime/protocol.gen'
|
||||
import type { APIKey, Account, AdminAccount, ErrorInfo, GitHubClient, GitHubIntent, ImportedTrack, MediaSort, Member, MyListen, MyPlaylist, MyPlaylistItem, QQLoginType, QRLogin, QRLoginResult, RoomListing, SharedPlaylist, Source, SourceAccount, StoredMedia, StoredMediaRef, Track } from '../realtime/protocol.gen'
|
||||
|
||||
export interface AddMyPlaylistItemRequest {
|
||||
track: Track
|
||||
@@ -65,8 +65,18 @@ export interface AdminGetYoutubeCredentialResponse {
|
||||
updatedAtMs: number
|
||||
}
|
||||
|
||||
export interface AdminListMediaRequest {
|
||||
page?: number
|
||||
q?: string
|
||||
sort?: MediaSort
|
||||
}
|
||||
|
||||
export interface AdminListMediaResponse {
|
||||
items: StoredMedia[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
totalBytes: number
|
||||
}
|
||||
|
||||
export interface AdminListRoomsResponse {
|
||||
@@ -75,6 +85,15 @@ export interface AdminListRoomsResponse {
|
||||
|
||||
export type AdminOpenRegistrationRequest = Record<string, never>
|
||||
|
||||
export interface AdminRemoveMediaRequest {
|
||||
media: StoredMediaRef[]
|
||||
}
|
||||
|
||||
export interface AdminRemoveMediaResponse {
|
||||
deleted: string[]
|
||||
inUse: string[]
|
||||
}
|
||||
|
||||
export type AdminResetAccountPasswordRequest = Record<string, never>
|
||||
|
||||
export interface AdminResetAccountPasswordResponse {
|
||||
@@ -391,8 +410,8 @@ export function httpClient(send: HttpSend) {
|
||||
adminGetGithubCredential: () => send<AdminGetGithubCredentialResponse>({ method: 'GET', path: '/api/admin/credentials/github', auth: 'admin' }),
|
||||
adminSetGithubCredential: (body: AdminSetGithubCredentialRequest) => send<void>({ method: 'PUT', path: '/api/admin/credentials/github', auth: 'admin', body }),
|
||||
adminDeleteGithubCredential: () => send<void>({ method: 'DELETE', path: '/api/admin/credentials/github', auth: 'admin' }),
|
||||
adminListMedia: () => send<AdminListMediaResponse>({ method: 'GET', path: '/api/admin/media', auth: 'admin' }),
|
||||
adminDeleteMedia: (key: string) => send<void>({ method: 'DELETE', path: `/api/admin/media/${encodeURIComponent(key)}`, auth: 'admin' }),
|
||||
adminListMedia: (body: AdminListMediaRequest) => send<AdminListMediaResponse>({ method: 'POST', path: '/api/admin/media/queries', auth: 'admin', body }),
|
||||
adminRemoveMedia: (body: AdminRemoveMediaRequest) => send<AdminRemoveMediaResponse>({ method: 'POST', path: '/api/admin/media/removals', auth: 'admin', body }),
|
||||
adminUpdateSite: (body: AdminUpdateSiteRequest) => send<AdminUpdateSiteResponse>({ method: 'PATCH', path: '/api/admin/site', auth: 'admin', body }),
|
||||
adminOpenRegistration: () => send<void>({ method: 'PUT', path: '/api/admin/registration', auth: 'admin', body: {} }),
|
||||
adminCloseRegistration: () => send<void>({ method: 'DELETE', path: '/api/admin/registration', auth: 'admin' }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
export const PROTOCOL_VERSION = '6bee23791ffa52ad'
|
||||
export const PROTOCOL_VERSION = 'ce462456c63356c6'
|
||||
|
||||
export interface APIKey {
|
||||
key: string
|
||||
@@ -76,6 +76,8 @@ export interface ImportedTrack {
|
||||
track: Track
|
||||
}
|
||||
|
||||
export type MediaSort = 'stored' | 'idle' | 'size'
|
||||
|
||||
export interface Member {
|
||||
avatarHash: string
|
||||
id: string
|
||||
@@ -308,6 +310,8 @@ export interface SourceAccount {
|
||||
}
|
||||
|
||||
export interface StoredMedia {
|
||||
artist: string
|
||||
coverUrl: string
|
||||
key: string
|
||||
lastPlayedAtMs: number | null
|
||||
link: string
|
||||
@@ -317,6 +321,10 @@ export interface StoredMedia {
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface StoredMediaRef {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface Track {
|
||||
id: string
|
||||
source: Source
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { AdminListMediaResponse } from '@/core/api'
|
||||
import type { StoredMedia as Media } from '@/core/realtime'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '@/core/api'
|
||||
import { bookTitle } from '@/shared/lib/book-title'
|
||||
import { formatDateTime } from '@/shared/lib/time'
|
||||
import { ConfirmDialog, IconButton, Skeleton } from '@/shared/ui'
|
||||
import { ConfirmDialog, IconButton, notificationStore, Skeleton } from '@/shared/ui'
|
||||
import { adminStore } from './admin-store'
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
@@ -12,17 +13,17 @@ function formatSize(bytes: number): string {
|
||||
}
|
||||
|
||||
export function StoredMedia() {
|
||||
const [media, setMedia] = useState<Media[] | null>(null)
|
||||
const [media, setMedia] = useState<AdminListMediaResponse | null>(null)
|
||||
const [failed, setFailed] = useState(false)
|
||||
const [reload, setReload] = useState(0)
|
||||
const [deleting, setDeleting] = useState<Media | null>(null)
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
void adminStore.call(() => api.adminListMedia()).then((result) => {
|
||||
void adminStore.call(() => api.adminListMedia({ page: 1 })).then((result) => {
|
||||
if (!active)
|
||||
return
|
||||
if (result)
|
||||
setMedia(result.items)
|
||||
setMedia(result)
|
||||
else
|
||||
setFailed(true)
|
||||
})
|
||||
@@ -35,12 +36,11 @@ export function StoredMedia() {
|
||||
setMedia(null)
|
||||
setReload(n => n + 1)
|
||||
}
|
||||
const totalSize = media?.reduce((sum, item) => sum + item.size, 0) ?? 0
|
||||
return (
|
||||
<section className="admin-section" aria-labelledby="admin-media-title">
|
||||
<div className="section-heading">
|
||||
<h2 id="admin-media-title" className="text-ui-lg">已存储的媒体</h2>
|
||||
{media && <span className="text-ui-sm muted">{media.length ? `${media.length} 首 · ${formatSize(totalSize)}` : '0 首'}</span>}
|
||||
{media && <span className="text-ui-sm muted">{media.total ? `${media.total} 首 · ${formatSize(media.totalBytes)}` : '0 首'}</span>}
|
||||
</div>
|
||||
{failed
|
||||
? (
|
||||
@@ -51,11 +51,11 @@ export function StoredMedia() {
|
||||
)
|
||||
: !media
|
||||
? <Skeleton rows={3} />
|
||||
: !media.length
|
||||
: !media.items.length
|
||||
? <p className="admin-empty text-ui-caption muted">还没有存储任何歌曲。</p>
|
||||
: (
|
||||
<ul className="admin-list">
|
||||
{media.map(item => (
|
||||
{media.items.map(item => (
|
||||
<li className="admin-row" key={item.key}>
|
||||
<div className="admin-row-copy">
|
||||
<strong className="text-ui-base truncate">{item.title}</strong>
|
||||
@@ -75,11 +75,16 @@ export function StoredMedia() {
|
||||
confirmLabel="删除"
|
||||
danger
|
||||
onConfirm={async () => {
|
||||
if (await adminStore.call(() => api.adminDeleteMedia(deleting.key)) !== null) {
|
||||
setDeleting(null)
|
||||
setFailed(false)
|
||||
setReload(n => n + 1)
|
||||
const result = await adminStore.call(() => api.adminRemoveMedia({ media: [{ key: deleting.key }] }))
|
||||
if (result === null)
|
||||
return
|
||||
if (result.inUse.length) {
|
||||
notificationStore.error('这首歌还在房间的播放列表中,不能删除')
|
||||
return
|
||||
}
|
||||
setDeleting(null)
|
||||
setFailed(false)
|
||||
setReload(n => n + 1)
|
||||
}}
|
||||
onClose={() => setDeleting(null)}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user