mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
Add commit message generation
This commit is contained in:
@@ -79,6 +79,10 @@ func embyWithRequestAddress(c *gin.Context, payload map[string]any) map[string]a
|
|||||||
if address := embyRequestBaseURL(c); address != "" {
|
if address := embyRequestBaseURL(c); address != "" {
|
||||||
out["LocalAddress"] = address
|
out["LocalAddress"] = address
|
||||||
out["WanAddress"] = address
|
out["WanAddress"] = address
|
||||||
|
out["PublishedServerUrl"] = address
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.ToLower(c.Request.URL.Path), "/emby") {
|
||||||
|
out["ProductName"] = "Emby Server"
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,34 +129,49 @@ func historyContinueHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
// historyDeleteHandler removes one or all history rows for the caller.
|
// historyDeleteHandler removes one or all history rows for the caller.
|
||||||
//
|
//
|
||||||
// DELETE /api/watch-history?media_id=xxx → delete just that media's row
|
// DELETE /api/watch-history?media_id=xxx → delete just that media's row
|
||||||
|
// DELETE /api/watch-history?status=completed → delete completed rows
|
||||||
|
// DELETE /api/watch-history?status=incomplete → delete unfinished rows
|
||||||
// DELETE /api/watch-history → clear all rows for the user
|
// DELETE /api/watch-history → clear all rows for the user
|
||||||
func historyDeleteHandler(svc *service.Container) gin.HandlerFunc {
|
func historyDeleteHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
uid, _ := c.Get(middleware.CtxUserID)
|
uid, _ := c.Get(middleware.CtxUserID)
|
||||||
userID := toString(uid)
|
userID := toString(uid)
|
||||||
mediaID := c.Query("media_id")
|
mediaID := c.Query("media_id")
|
||||||
|
status := c.Query("status")
|
||||||
|
|
||||||
q := svc.Repo.DB.Where("user_id = ?", userID)
|
q := svc.Repo.DB.Where("user_id = ?", userID)
|
||||||
if mediaID != "" {
|
if mediaID != "" {
|
||||||
q = q.Where("media_id = ?", mediaID)
|
q = q.Where("media_id = ?", mediaID)
|
||||||
}
|
}
|
||||||
if err := q.Delete(&model.PlaybackHistory{}).Error; err != nil {
|
switch status {
|
||||||
|
case "completed", "watched":
|
||||||
|
q = q.Where("completed = ?", true)
|
||||||
|
case "incomplete", "unfinished", "unwatched":
|
||||||
|
q = q.Where("completed = ?", false)
|
||||||
|
case "":
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "status must be completed or incomplete"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := q.Delete(&model.PlaybackHistory{})
|
||||||
|
if err := res.Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Status(http.StatusNoContent)
|
c.JSON(http.StatusOK, gin.H{"removed": res.RowsAffected})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func historyDeleteOneHandler(svc *service.Container) gin.HandlerFunc {
|
func historyDeleteOneHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
uid, _ := c.Get(middleware.CtxUserID)
|
uid, _ := c.Get(middleware.CtxUserID)
|
||||||
if err := svc.Repo.DB.
|
res := svc.Repo.DB.
|
||||||
Where("user_id = ? AND id = ?", toString(uid), c.Param("id")).
|
Where("user_id = ? AND id = ?", toString(uid), c.Param("id")).
|
||||||
Delete(&model.PlaybackHistory{}).Error; err != nil {
|
Delete(&model.PlaybackHistory{})
|
||||||
|
if err := res.Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Status(http.StatusNoContent)
|
c.JSON(http.StatusOK, gin.H{"removed": res.RowsAffected})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ export const historyAPI = {
|
|||||||
})
|
})
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
clear: (mediaID?: string) =>
|
clear: (mediaID?: string, status?: 'completed' | 'incomplete') =>
|
||||||
api
|
api
|
||||||
.delete('/watch-history', {
|
.delete('/watch-history', {
|
||||||
params: mediaID ? { media_id: mediaID } : {},
|
params: {
|
||||||
|
...(mediaID ? { media_id: mediaID } : {}),
|
||||||
|
...(status ? { status } : {}),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Clock, Play } from 'lucide-react'
|
import { Clock, Play, Trash2 } from 'lucide-react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import { playbackAPI, type HistoryItem } from '../api/playback'
|
import { historyAPI } from '../api/history'
|
||||||
import { imageURL } from '../api/client'
|
import { imageURL } from '../api/client'
|
||||||
|
import type { HistoryItem } from '../types'
|
||||||
|
|
||||||
function fmtDuration(ms: number): string {
|
function fmtDuration(ms: number): string {
|
||||||
if (!ms || ms <= 0) return '—'
|
if (!ms || ms <= 0) return '—'
|
||||||
@@ -18,19 +20,66 @@ function fmtDuration(ms: number): string {
|
|||||||
export function WatchHistoryPage() {
|
export function WatchHistoryPage() {
|
||||||
const [items, setItems] = useState<HistoryItem[]>([])
|
const [items, setItems] = useState<HistoryItem[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [busy, setBusy] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
const load = () => {
|
||||||
playbackAPI
|
setLoading(true)
|
||||||
.recentHistory()
|
historyAPI
|
||||||
|
.list(200)
|
||||||
.then(setItems)
|
.then(setItems)
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
|
useEffect(() => { load() }, [])
|
||||||
|
|
||||||
|
const removeOne = async (id: string) => {
|
||||||
|
if (!confirm('确定移除此条观看历史?不会删除媒体文件。')) return
|
||||||
|
setBusy(id)
|
||||||
|
try {
|
||||||
|
await historyAPI.remove(id)
|
||||||
|
setItems((prev) => prev.filter((item) => item.id !== id))
|
||||||
|
toast.success('已移除观看历史')
|
||||||
|
} finally {
|
||||||
|
setBusy('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearByStatus = async (status: 'completed' | 'incomplete') => {
|
||||||
|
const label = status === 'completed' ? '已看完' : '未看完'
|
||||||
|
if (!confirm(`确定清除所有${label}的观看历史?不会删除媒体文件。`)) return
|
||||||
|
setBusy(status)
|
||||||
|
try {
|
||||||
|
await historyAPI.clear(undefined, status)
|
||||||
|
setItems((prev) => prev.filter((item) => status === 'completed' ? !item.completed : item.completed))
|
||||||
|
toast.success(`已清除${label}记录`)
|
||||||
|
} finally {
|
||||||
|
setBusy('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<header className="flex items-center gap-3">
|
<header className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<Clock className="h-6 w-6 text-brand-500" />
|
<Clock className="h-6 w-6 text-brand-500" />
|
||||||
<h1 className="font-display text-3xl font-bold text-ink-600">观看历史</h1>
|
<h1 className="font-display text-3xl font-bold text-ink-600">观看历史</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => clearByStatus('incomplete')}
|
||||||
|
disabled={busy !== '' || items.every((item) => item.completed)}
|
||||||
|
className="rounded-xl border border-gray-200 bg-white px-3 py-2 text-xs font-bold text-gray-600 shadow-sm transition hover:border-amber-300 hover:text-amber-600 disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
|
>
|
||||||
|
清除未看完
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => clearByStatus('completed')}
|
||||||
|
disabled={busy !== '' || items.every((item) => !item.completed)}
|
||||||
|
className="rounded-xl border border-gray-200 bg-white px-3 py-2 text-xs font-bold text-gray-600 shadow-sm transition hover:border-emerald-300 hover:text-emerald-600 disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
|
>
|
||||||
|
清除已看完
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{loading && <p className="text-sand-500">加载中…</p>}
|
{loading && <p className="text-sand-500">加载中…</p>}
|
||||||
@@ -82,12 +131,23 @@ export function WatchHistoryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
to={`/play/${m.id}`}
|
to={`/play/${m.id}`}
|
||||||
className="neon-button !px-3 !py-1 !text-xs"
|
className="neon-button !px-3 !py-1 !text-xs"
|
||||||
>
|
>
|
||||||
<Play size={12} /> 继续
|
<Play size={12} /> 继续
|
||||||
</Link>
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => removeOne(h.id)}
|
||||||
|
disabled={busy === h.id}
|
||||||
|
className="inline-flex items-center gap-1 rounded-xl border border-gray-200 bg-white px-3 py-1.5 text-xs font-bold text-gray-500 transition hover:border-red-200 hover:bg-red-50 hover:text-red-500 disabled:opacity-50"
|
||||||
|
title="移除此条观看历史"
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
移除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user