mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
优化
This commit is contained in:
@@ -20,6 +20,9 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
if err := ensureLibraryRootsCompatibility(db); err != nil {
|
if err := ensureLibraryRootsCompatibility(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := ensureEmbyMountsCompatibility(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if isSQLite(db) {
|
if isSQLite(db) {
|
||||||
return ensureMediaSearchIndex(db)
|
return ensureMediaSearchIndex(db)
|
||||||
}
|
}
|
||||||
@@ -77,3 +80,25 @@ func ensurePerformanceIndexes(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureEmbyMountsCompatibility(db *gorm.DB) error {
|
||||||
|
if !db.Migrator().HasTable(&model.EmbyMount{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !db.Migrator().HasColumn(&model.EmbyMount{}, "sort_order") {
|
||||||
|
if err := db.Migrator().AddColumn(&model.EmbyMount{}, "sort_order"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 针对已有数据:如果存在多个 sort_order=0/NULL 的记录,按创建时间顺序赋予稳定递增的序号
|
||||||
|
var zeroCount int64
|
||||||
|
if err := db.Model(&model.EmbyMount{}).Where("sort_order = 0 OR sort_order IS NULL").Count(&zeroCount).Error; err == nil && zeroCount > 1 {
|
||||||
|
var mounts []model.EmbyMount
|
||||||
|
if err := db.Order("created_at asc, id asc").Find(&mounts).Error; err == nil {
|
||||||
|
for i, m := range mounts {
|
||||||
|
_ = db.Exec("UPDATE emby_mounts SET sort_order = ? WHERE id = ?", i, m.ID).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnsureEmbyMountsCompatibility(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a table without sort_order simulating an older schema
|
||||||
|
if err := db.Exec(`CREATE TABLE emby_mounts (
|
||||||
|
id varchar(36) PRIMARY KEY,
|
||||||
|
created_at datetime,
|
||||||
|
updated_at datetime,
|
||||||
|
deleted_at datetime,
|
||||||
|
account_id text,
|
||||||
|
remote_view_id text,
|
||||||
|
remote_view_name text,
|
||||||
|
collection_type text,
|
||||||
|
name text,
|
||||||
|
proxy_play numeric DEFAULT false,
|
||||||
|
enabled numeric DEFAULT true
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert older rows
|
||||||
|
now := time.Now()
|
||||||
|
_ = db.Exec("INSERT INTO emby_mounts (id, name, created_at) VALUES (?, ?, ?)", "m1", "Mount 1", now.Add(-2*time.Hour)).Error
|
||||||
|
_ = db.Exec("INSERT INTO emby_mounts (id, name, created_at) VALUES (?, ?, ?)", "m2", "Mount 2", now.Add(-1*time.Hour)).Error
|
||||||
|
|
||||||
|
// Run compatibility migration
|
||||||
|
if err := ensureEmbyMountsCompatibility(db); err != nil {
|
||||||
|
t.Fatalf("ensureEmbyMountsCompatibility failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify column sort_order exists and values are initialized sequentially
|
||||||
|
if !db.Migrator().HasColumn(&model.EmbyMount{}, "sort_order") {
|
||||||
|
t.Fatal("expected sort_order column to be added")
|
||||||
|
}
|
||||||
|
|
||||||
|
var m1, m2 model.EmbyMount
|
||||||
|
if err := db.Where("id = ?", "m1").First(&m1).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.Where("id = ?", "m2").First(&m2).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m1.SortOrder != 0 || m2.SortOrder != 1 {
|
||||||
|
t.Fatalf("unexpected sort orders: m1=%d, m2=%d", m1.SortOrder, m2.SortOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
@@ -146,13 +147,13 @@ func (r *EmbyRemoteService) mapRemoteMountToLibrary(mount *model.EmbyMount, acct
|
|||||||
case "music":
|
case "music":
|
||||||
libType = "music"
|
libType = "music"
|
||||||
}
|
}
|
||||||
lib := &model.Library{
|
lib := &model.Library{
|
||||||
Base: model.Base{ID: EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID)},
|
Base: model.Base{ID: EncodeEmbyRemoteID(mount.ID, mount.RemoteViewID)},
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: libType,
|
Type: libType,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
SortOrder: 1000 + mount.SortOrder, // 远程库排在本地库之后,且保持挂载库排序
|
SortOrder: 1000 + mount.SortOrder, // 远程库排在本地库之后,且保持挂载库排序
|
||||||
}
|
}
|
||||||
// 远程媒体库封面只有真实存在图片标签才下发。
|
// 远程媒体库封面只有真实存在图片标签才下发。
|
||||||
if remoteItemHasImageTag(item, "Primary") {
|
if remoteItemHasImageTag(item, "Primary") {
|
||||||
lib.CoverURL = r.remoteItemImageURL(cfg, mount.RemoteViewID, "Primary")
|
lib.CoverURL = r.remoteItemImageURL(cfg, mount.RemoteViewID, "Primary")
|
||||||
@@ -177,17 +178,25 @@ func (r *EmbyRemoteService) MapRemoteItemToMedia(ctx context.Context, mount *mod
|
|||||||
if _, rid, ok := DecodeEmbyRemoteID(seriesID); ok {
|
if _, rid, ok := DecodeEmbyRemoteID(seriesID); ok {
|
||||||
seriesID = rid
|
seriesID = rid
|
||||||
}
|
}
|
||||||
|
rating := remoteItemFloat(item, "CommunityRating")
|
||||||
|
if rating == 0 {
|
||||||
|
rating = remoteItemFloat(item, "CriticRating")
|
||||||
|
}
|
||||||
media := model.Media{
|
media := model.Media{
|
||||||
Base: model.Base{ID: EncodeEmbyRemoteID(encodeScope, remoteID)},
|
Base: model.Base{ID: EncodeEmbyRemoteID(encodeScope, remoteID)},
|
||||||
Title: remoteItemString(item, "Name"),
|
Title: remoteItemString(item, "Name"),
|
||||||
OriginalName: remoteItemString(item, "OriginalTitle"),
|
OriginalName: remoteItemString(item, "OriginalTitle"),
|
||||||
Overview: remoteItemString(item, "Overview"),
|
Overview: remoteItemString(item, "Overview"),
|
||||||
Year: remoteItemInt(item, "ProductionYear"),
|
Year: remoteItemInt(item, "ProductionYear"),
|
||||||
Rating: float32(remoteItemFloat(item, "CommunityRating")),
|
Rating: float32(rating),
|
||||||
Path: remoteItemString(item, "Path"),
|
Path: remoteItemString(item, "Path"),
|
||||||
Genres: remoteItemGenres(item),
|
Genres: remoteItemGenres(item),
|
||||||
ScrapeStatus: "done",
|
ScrapeStatus: "done",
|
||||||
}
|
}
|
||||||
|
if date, ok := parseEmbyRemoteDate(remoteItemString(item, "DateCreated")); ok {
|
||||||
|
media.CreatedAt = date
|
||||||
|
media.UpdatedAt = date
|
||||||
|
}
|
||||||
// 只有远程明确存在图片标签才下发图片 URL。
|
// 只有远程明确存在图片标签才下发图片 URL。
|
||||||
if remoteItemHasImageTag(item, "Primary") {
|
if remoteItemHasImageTag(item, "Primary") {
|
||||||
media.PosterURL = r.remoteItemImageURL(cfg, remoteID, "Primary")
|
media.PosterURL = r.remoteItemImageURL(cfg, remoteID, "Primary")
|
||||||
@@ -198,8 +207,16 @@ func (r *EmbyRemoteService) MapRemoteItemToMedia(ctx context.Context, mount *mod
|
|||||||
if ticks := remoteItemInt64(item, "RunTimeTicks"); ticks > 0 {
|
if ticks := remoteItemInt64(item, "RunTimeTicks"); ticks > 0 {
|
||||||
media.DurationSec = int(ticks / 10_000_000)
|
media.DurationSec = int(ticks / 10_000_000)
|
||||||
}
|
}
|
||||||
if date, ok := embyPremiereDate(remoteItemString(item, "PremiereDate")); ok {
|
if date, ok := parseEmbyRemoteDate(remoteItemString(item, "PremiereDate")); ok {
|
||||||
media.ReleaseDate = date.Format("2006-01-02")
|
media.ReleaseDate = date.Format("2006-01-02")
|
||||||
|
if media.Year == 0 {
|
||||||
|
media.Year = date.Year()
|
||||||
|
}
|
||||||
|
} else if date, ok := embyPremiereDate(remoteItemString(item, "PremiereDate")); ok {
|
||||||
|
media.ReleaseDate = date.Format("2006-01-02")
|
||||||
|
if media.Year == 0 {
|
||||||
|
media.Year = date.Year()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if providerIDs, ok := item["ProviderIds"].(map[string]any); ok {
|
if providerIDs, ok := item["ProviderIds"].(map[string]any); ok {
|
||||||
if v := anyString(providerIDs["Tmdb"]); v != "" {
|
if v := anyString(providerIDs["Tmdb"]); v != "" {
|
||||||
@@ -309,7 +326,7 @@ func (r *EmbyRemoteService) RemoteLibraryMedia(ctx context.Context, mount *model
|
|||||||
q.Set("Recursive", "false")
|
q.Set("Recursive", "false")
|
||||||
q.Set("StartIndex", strconv.Itoa(offset))
|
q.Set("StartIndex", strconv.Itoa(offset))
|
||||||
q.Set("Limit", strconv.Itoa(limit))
|
q.Set("Limit", strconv.Itoa(limit))
|
||||||
q.Set("Fields", "Overview,Genres,ProviderIds,Path,SeriesPrimaryImage,MediaStreams,MediaSources")
|
q.Set("Fields", "Overview,Genres,ProviderIds,Path,SeriesPrimaryImage,MediaStreams,MediaSources,DateCreated,PremiereDate,ProductionYear,CommunityRating,CriticRating")
|
||||||
var body struct {
|
var body struct {
|
||||||
Items []map[string]any `json:"Items"`
|
Items []map[string]any `json:"Items"`
|
||||||
TotalRecordCount int64 `json:"TotalRecordCount"`
|
TotalRecordCount int64 `json:"TotalRecordCount"`
|
||||||
@@ -338,7 +355,7 @@ func (r *EmbyRemoteService) RemoteMediaDetail(ctx context.Context, mount *model.
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
path := "/Users/" + url.PathEscape(r.remoteUserID(cfg)) + "/Items/" + url.PathEscape(remoteID)
|
path := "/Users/" + url.PathEscape(r.remoteUserID(cfg)) + "/Items/" + url.PathEscape(remoteID)
|
||||||
path += "?Fields=Overview,Genres,ProviderIds,People,Studios,Path,MediaStreams,MediaSources"
|
path += "?Fields=Overview,Genres,ProviderIds,People,Studios,Path,MediaStreams,MediaSources,DateCreated,PremiereDate,ProductionYear,CommunityRating,CriticRating"
|
||||||
var out map[string]any
|
var out map[string]any
|
||||||
if err := r.doGet(ctx, acct, cfg, path, nil, &out); err != nil {
|
if err := r.doGet(ctx, acct, cfg, path, nil, &out); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -399,7 +416,7 @@ func (r *EmbyRemoteService) remoteEpisodesOf(ctx context.Context, mount *model.E
|
|||||||
q.Set("Recursive", "true")
|
q.Set("Recursive", "true")
|
||||||
q.Set("StartIndex", "0")
|
q.Set("StartIndex", "0")
|
||||||
q.Set("Limit", "500")
|
q.Set("Limit", "500")
|
||||||
q.Set("Fields", "Overview,Genres,ProviderIds,Path,SeriesPrimaryImage,MediaStreams,MediaSources")
|
q.Set("Fields", "Overview,Genres,ProviderIds,Path,SeriesPrimaryImage,MediaStreams,MediaSources,DateCreated,PremiereDate,ProductionYear,CommunityRating,CriticRating")
|
||||||
var body struct {
|
var body struct {
|
||||||
Items []map[string]any `json:"Items"`
|
Items []map[string]any `json:"Items"`
|
||||||
TotalRecordCount int64 `json:"TotalRecordCount"`
|
TotalRecordCount int64 `json:"TotalRecordCount"`
|
||||||
@@ -433,7 +450,7 @@ func (r *EmbyRemoteService) RemoteSeriesCards(ctx context.Context, mount *model.
|
|||||||
q.Set("Recursive", "false")
|
q.Set("Recursive", "false")
|
||||||
q.Set("StartIndex", "0")
|
q.Set("StartIndex", "0")
|
||||||
q.Set("Limit", "1000")
|
q.Set("Limit", "1000")
|
||||||
q.Set("Fields", "Overview,Genres,ProviderIds,Path,RecursiveItemCount,SeriesPrimaryImage")
|
q.Set("Fields", "Overview,Genres,ProviderIds,Path,RecursiveItemCount,SeriesPrimaryImage,DateCreated,PremiereDate,ProductionYear,CommunityRating,CriticRating")
|
||||||
var body struct {
|
var body struct {
|
||||||
Items []map[string]any `json:"Items"`
|
Items []map[string]any `json:"Items"`
|
||||||
}
|
}
|
||||||
@@ -696,3 +713,23 @@ func RemoteItemCollectionType(item map[string]any) string {
|
|||||||
|
|
||||||
// RemoteItemChildCount 提取远程 View 条目的 ChildCount。
|
// RemoteItemChildCount 提取远程 View 条目的 ChildCount。
|
||||||
func RemoteItemChildCount(item map[string]any) int { return remoteItemInt(item, "ChildCount") }
|
func RemoteItemChildCount(item map[string]any) int { return remoteItemInt(item, "ChildCount") }
|
||||||
|
|
||||||
|
func parseEmbyRemoteDate(s string) (time.Time, bool) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
for _, layout := range []string{
|
||||||
|
time.RFC3339Nano,
|
||||||
|
time.RFC3339,
|
||||||
|
"2006-01-02T15:04:05.9999999Z",
|
||||||
|
"2006-01-02T15:04:05.9999999",
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
"2006-01-02",
|
||||||
|
} {
|
||||||
|
if t, err := time.Parse(layout, s); err == nil {
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapRemoteItemToMediaSortingFields(t *testing.T) {
|
||||||
|
svc := &EmbyRemoteService{}
|
||||||
|
mount := &model.EmbyMount{Base: model.Base{ID: "mount-1"}}
|
||||||
|
acct := &model.StrmAccount{Base: model.Base{ID: "acct-1"}}
|
||||||
|
cfg := &EmbyRemoteConfig{BaseURL: "http://localhost:8096"}
|
||||||
|
|
||||||
|
item := map[string]any{
|
||||||
|
"Id": "item-1",
|
||||||
|
"Name": "测试电影",
|
||||||
|
"OriginalTitle": "Test Movie",
|
||||||
|
"ProductionYear": 2023,
|
||||||
|
"CommunityRating": 8.5,
|
||||||
|
"PremiereDate": "2023-05-12T00:00:00.0000000Z",
|
||||||
|
"DateCreated": "2024-01-15T08:30:00.0000000Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
media := svc.MapRemoteItemToMedia(context.Background(), mount, acct, cfg, item)
|
||||||
|
|
||||||
|
if media.ReleaseDate != "2023-05-12" {
|
||||||
|
t.Fatalf("ReleaseDate = %q, want %q", media.ReleaseDate, "2023-05-12")
|
||||||
|
}
|
||||||
|
if media.Year != 2023 {
|
||||||
|
t.Fatalf("Year = %d, want 2023", media.Year)
|
||||||
|
}
|
||||||
|
if media.Rating != 8.5 {
|
||||||
|
t.Fatalf("Rating = %f, want 8.5", media.Rating)
|
||||||
|
}
|
||||||
|
expectedCreated, _ := time.Parse(time.RFC3339, "2024-01-15T08:30:00Z")
|
||||||
|
if !media.CreatedAt.Equal(expectedCreated) {
|
||||||
|
t.Fatalf("CreatedAt = %v, want %v", media.CreatedAt, expectedCreated)
|
||||||
|
}
|
||||||
|
if !media.UpdatedAt.Equal(expectedCreated) {
|
||||||
|
t.Fatalf("UpdatedAt = %v, want %v", media.UpdatedAt, expectedCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapRemoteItemToMediaCriticRatingFallback(t *testing.T) {
|
||||||
|
svc := &EmbyRemoteService{}
|
||||||
|
mount := &model.EmbyMount{Base: model.Base{ID: "mount-1"}}
|
||||||
|
acct := &model.StrmAccount{Base: model.Base{ID: "acct-1"}}
|
||||||
|
cfg := &EmbyRemoteConfig{BaseURL: "http://localhost:8096"}
|
||||||
|
|
||||||
|
item := map[string]any{
|
||||||
|
"Id": "item-2",
|
||||||
|
"Name": "评分测试",
|
||||||
|
"CriticRating": 9.2,
|
||||||
|
"PremiereDate": "2022-10-01",
|
||||||
|
}
|
||||||
|
|
||||||
|
media := svc.MapRemoteItemToMedia(context.Background(), mount, acct, cfg, item)
|
||||||
|
if media.Rating != 9.2 {
|
||||||
|
t.Fatalf("Rating = %f, want 9.2 from CriticRating", media.Rating)
|
||||||
|
}
|
||||||
|
if media.Year != 2022 {
|
||||||
|
t.Fatalf("Year = %d, want 2022 from PremiereDate", media.Year)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -310,10 +310,11 @@ export function EmbyMountPage() {
|
|||||||
|
|
||||||
const applyReorder = async (nextMounts: EmbyMount[]) => {
|
const applyReorder = async (nextMounts: EmbyMount[]) => {
|
||||||
const prevMounts = mounts
|
const prevMounts = mounts
|
||||||
setMounts(nextMounts)
|
const updated = nextMounts.map((m, idx) => ({ ...m, sort_order: idx }))
|
||||||
|
setMounts(updated)
|
||||||
setReordering(true)
|
setReordering(true)
|
||||||
try {
|
try {
|
||||||
await embyAPI.reorderMounts(nextMounts.map((m) => m.id))
|
await embyAPI.reorderMounts(updated.map((m) => m.id))
|
||||||
toast.success('媒体库顺序已更新')
|
toast.success('媒体库顺序已更新')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMounts(prevMounts)
|
setMounts(prevMounts)
|
||||||
@@ -347,6 +348,7 @@ export function EmbyMountPage() {
|
|||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent, id: string) => {
|
const handleDragStart = (e: React.DragEvent, id: string) => {
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
e.dataTransfer.setData('text/plain', id)
|
||||||
setDraggingId(id)
|
setDraggingId(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,18 +360,18 @@ export function EmbyMountPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDrop = (overId: string, accountId: string) => {
|
const handleDrop = (e: React.DragEvent, overId: string, accountId: string) => {
|
||||||
if (!draggingId || draggingId === overId) {
|
e.preventDefault()
|
||||||
setDraggingId(null)
|
const fromId = draggingId || e.dataTransfer.getData('text/plain')
|
||||||
setDragOverId(null)
|
setDraggingId(null)
|
||||||
|
setDragOverId(null)
|
||||||
|
if (!fromId || fromId === overId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const acctMounts = mounts.filter((item) => item.account_id === accountId)
|
const acctMounts = mounts.filter((item) => item.account_id === accountId)
|
||||||
const fromIdx = acctMounts.findIndex((item) => item.id === draggingId)
|
const fromIdx = acctMounts.findIndex((item) => item.id === fromId)
|
||||||
const toIdx = acctMounts.findIndex((item) => item.id === overId)
|
const toIdx = acctMounts.findIndex((item) => item.id === overId)
|
||||||
if (fromIdx < 0 || toIdx < 0) {
|
if (fromIdx < 0 || toIdx < 0) {
|
||||||
setDraggingId(null)
|
|
||||||
setDragOverId(null)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newAcctMounts = [...acctMounts]
|
const newAcctMounts = [...acctMounts]
|
||||||
@@ -383,8 +385,6 @@ export function EmbyMountPage() {
|
|||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
setDraggingId(null)
|
|
||||||
setDragOverId(null)
|
|
||||||
applyReorder(nextMounts)
|
applyReorder(nextMounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +544,7 @@ export function EmbyMountPage() {
|
|||||||
<div
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
onDragOver={(e) => handleDragOver(e, m.id)}
|
onDragOver={(e) => handleDragOver(e, m.id)}
|
||||||
onDrop={() => handleDrop(m.id, acct.id)}
|
onDrop={(e) => handleDrop(e, m.id, acct.id)}
|
||||||
className={`flex items-center gap-3 rounded-xl border px-3 py-3 transition-colors ${
|
className={`flex items-center gap-3 rounded-xl border px-3 py-3 transition-colors ${
|
||||||
draggingId === m.id
|
draggingId === m.id
|
||||||
? 'border-dashed border-brand-400 bg-brand-50/20 opacity-50'
|
? 'border-dashed border-brand-400 bg-brand-50/20 opacity-50'
|
||||||
|
|||||||
Reference in New Issue
Block a user