bugfixes
This commit is contained in:
parent
98e7bc70f2
commit
43449405e8
BIN
Icons/boobjob.png
Normal file
BIN
Icons/boobjob.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
Icons/penis.png
BIN
Icons/penis.png
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.6 KiB |
@ -1,4 +1,4 @@
|
||||
# backend/ai_server.py
|
||||
# backend\ai_server.py
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
@ -1509,7 +1509,7 @@ func buildHighlightHitsFromPrediction(pred TrainingPrediction, t float64) []anal
|
||||
continue
|
||||
}
|
||||
|
||||
// Schwache Positions-Kontexte wie standing/sitting nicht alleine als Segment anzeigen.
|
||||
// Schwache Positions-Kontexte wie standing nicht alleine als Segment anzeigen.
|
||||
if sig.Group == "position" && segmentSeverityWeight(label) < 0.70 {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"items": []
|
||||
}
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
//go:embed ml/*.py ml/*.json ai_server.py .env recorder-cert.pem recorder-key.pem
|
||||
//go:embed ml/*.py ai_server.py .env recorder-cert.pem recorder-key.pem generated/training/detection_labels.json
|
||||
var embeddedMLFiles embed.FS
|
||||
|
||||
func trainingEmbeddedMLDir() (string, error) {
|
||||
@ -33,6 +33,10 @@ func trainingEmbeddedMLDir() (string, error) {
|
||||
for _, name := range append(files, optionalFiles...) {
|
||||
srcPath := filepath.ToSlash(filepath.Join("ml", name))
|
||||
|
||||
if name == "detection_labels.json" {
|
||||
srcPath = "generated/training/detection_labels.json"
|
||||
}
|
||||
|
||||
b, err := embeddedMLFiles.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
// Pflichtdateien müssen vorhanden sein.
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
{
|
||||
"people": [
|
||||
"person_female",
|
||||
"person_male"
|
||||
],
|
||||
"sexPositions": [
|
||||
"unknown",
|
||||
"missionary",
|
||||
"doggy",
|
||||
"cowgirl",
|
||||
"reverse_cowgirl",
|
||||
"cunnilingus",
|
||||
"prone_bone",
|
||||
"standing",
|
||||
"standing_doggy",
|
||||
"spooning",
|
||||
"facesitting",
|
||||
"handjob",
|
||||
"blowjob",
|
||||
"boobjob",
|
||||
"toy_play",
|
||||
"fingering",
|
||||
"69"
|
||||
],
|
||||
"bodyParts": [
|
||||
"anus",
|
||||
"ass",
|
||||
"breasts",
|
||||
"penis",
|
||||
"tongue",
|
||||
"pussy"
|
||||
],
|
||||
"objects": [
|
||||
"blindfold",
|
||||
"buttplug",
|
||||
"collar",
|
||||
"dildo",
|
||||
"handcuffs",
|
||||
"shower",
|
||||
"strapon",
|
||||
"towel",
|
||||
"vibrator"
|
||||
],
|
||||
"clothing": [
|
||||
"bikini",
|
||||
"bra",
|
||||
"dress",
|
||||
"heels",
|
||||
"hotpants",
|
||||
"lingerie",
|
||||
"panties",
|
||||
"skirt",
|
||||
"stockings",
|
||||
"croptop"
|
||||
]
|
||||
}
|
||||
@ -451,7 +451,7 @@ func contextualSegmentSeverityWeight(label string) float64 {
|
||||
score += 0.015
|
||||
}
|
||||
|
||||
// Standing/Sitting ohne Kontext bleibt schwach.
|
||||
// Standing ohne Kontext bleibt schwach.
|
||||
if set.Position < 0.60 && !set.HasBody && !set.HasObject && !set.HasClothing {
|
||||
score = math.Min(score, 0.42)
|
||||
}
|
||||
|
||||
@ -464,9 +464,22 @@ func startAIServer(ctx context.Context) (*aiServerProcess, error) {
|
||||
|
||||
trainingRoot, appBaseDir := findTrainingRootDir(scriptDir)
|
||||
|
||||
mlDir, mlErr := trainingEmbeddedMLDir()
|
||||
if mlErr != nil {
|
||||
appLogln("⚠️ embedded ML-Dateien konnten nicht extrahiert werden:", mlErr)
|
||||
} else {
|
||||
appLogln("✅ embedded ML-Dateien extrahiert:", mlDir)
|
||||
}
|
||||
|
||||
detectionLabelsPath := strings.TrimSpace(os.Getenv("DETECTION_LABELS_PATH"))
|
||||
if detectionLabelsPath == "" {
|
||||
detectionLabelsPath = filepath.Join(trainingRoot, "detection_labels.json")
|
||||
if mlErr == nil && strings.TrimSpace(mlDir) != "" {
|
||||
// ai_server.py soll die entpackte embedded detection_labels.json verwenden.
|
||||
detectionLabelsPath = filepath.Join(mlDir, "detection_labels.json")
|
||||
} else {
|
||||
// Fallback für Entwicklung / falls embedded extraction fehlschlägt.
|
||||
detectionLabelsPath = filepath.Join(trainingRoot, "detection_labels.json")
|
||||
}
|
||||
} else {
|
||||
detectionLabelsPath = filepath.Clean(detectionLabelsPath)
|
||||
}
|
||||
|
||||
@ -4301,7 +4301,7 @@ export default function App() {
|
||||
setSplitModalOpen(false)
|
||||
setSplitJob(null)
|
||||
}}
|
||||
onApply={({ job, splits, segments, deleteOriginal }) => {
|
||||
onApply={async ({ job, splits, segments, deleteOriginal, onProgress }) => {
|
||||
const file = baseName(job.output || '')
|
||||
if (!file) {
|
||||
throw new Error('Kein Dateiname vorhanden.')
|
||||
@ -4338,13 +4338,18 @@ export default function App() {
|
||||
return segments[pos - 1] ?? null
|
||||
}
|
||||
|
||||
const publish = (
|
||||
progress: Omit<BackgroundSplitProgress, 'sourceFile' | 'startedAtMs'>
|
||||
) => {
|
||||
pushSplitProgress(file, progress)
|
||||
type ModalSplitProgress = Parameters<NonNullable<typeof onProgress>>[0]
|
||||
|
||||
const publish = (progress: ModalSplitProgress) => {
|
||||
pushSplitProgress(
|
||||
file,
|
||||
progress as Omit<BackgroundSplitProgress, 'sourceFile' | 'startedAtMs'>
|
||||
)
|
||||
|
||||
onProgress?.(progress)
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
return await (async () => {
|
||||
let buffer = ''
|
||||
let finalData: any = null
|
||||
let total = totalFallback
|
||||
@ -4562,6 +4567,7 @@ export default function App() {
|
||||
})
|
||||
|
||||
notify.error('Split fehlgeschlagen', `${file} — ${message}`)
|
||||
throw err
|
||||
}
|
||||
})()
|
||||
}}
|
||||
|
||||
@ -20,7 +20,6 @@ export const AI_POSITION_LABELS = [
|
||||
'standing',
|
||||
'standing_doggy',
|
||||
'spooning',
|
||||
'sitting',
|
||||
'facesitting',
|
||||
'handjob',
|
||||
'blowjob',
|
||||
@ -121,7 +120,6 @@ export const AI_LABEL_FALLBACKS: Record<string, string> = {
|
||||
standing: 'Stehend',
|
||||
standing_doggy: 'Standing Doggy',
|
||||
spooning: 'Spooning',
|
||||
sitting: 'Sitzend',
|
||||
facesitting: 'Facesitting',
|
||||
handjob: 'Handjob',
|
||||
blowjob: 'Blowjob',
|
||||
|
||||
@ -1324,10 +1324,6 @@ function FinishedDownloadsCardsView({
|
||||
doubleTapMaxMovePx={48}
|
||||
pressDelayMs={220}
|
||||
enablePressHold={canSpeedHold}
|
||||
verticalThresholdPx={92}
|
||||
verticalThresholdRatio={0.20}
|
||||
upAction={{ label: 'Öffnen' }}
|
||||
downAction={{ label: 'Weiter' }}
|
||||
onDoubleTap={async () => {
|
||||
if (isHot) return
|
||||
await onToggleHot?.(j)
|
||||
@ -1361,16 +1357,6 @@ function FinishedDownloadsCardsView({
|
||||
setMobileTopTeaserEnabled(false)
|
||||
return await keepVideo(j)
|
||||
}}
|
||||
onSwipeUp={() => {
|
||||
setMobileTopTeaserEnabled(false)
|
||||
openPlayer(j)
|
||||
return false
|
||||
}}
|
||||
onSwipeDown={() => {
|
||||
setMobileTopTeaserEnabled(false)
|
||||
skipTopMobileCard()
|
||||
return true
|
||||
}}
|
||||
onPressStart={(info) => {
|
||||
if (info.pointerType === 'mouse' && info.button !== 0) return
|
||||
if (!canSpeedHold) return
|
||||
@ -1397,10 +1383,6 @@ function FinishedDownloadsCardsView({
|
||||
|
||||
{mobileTopRow ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="text-center text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
||||
← Löschen · → Behalten · ↑ Öffnen · ↓ Weiter · Doppeltipp HOT · Halten schneller
|
||||
</div>
|
||||
|
||||
{mobileOrderedRows.length > 1 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@ -64,38 +64,18 @@ export function FemaleBreastIcon(props: IconProps) {
|
||||
}
|
||||
|
||||
export function BoobjobIcon(props: IconProps) {
|
||||
const { title, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={iconClassName(className, 'origin-center scale-[1.12]')}
|
||||
aria-hidden={title ? undefined : true}
|
||||
role={title ? 'img' : undefined}
|
||||
{...rest}
|
||||
<IconBase
|
||||
{...props}
|
||||
className={iconClassName(props.className, 'origin-center scale-[1.12]')}
|
||||
viewBox="0 0 211 158"
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
|
||||
<FemaleBreastIcon
|
||||
x="3"
|
||||
y="20"
|
||||
width="94"
|
||||
height="58"
|
||||
aria-hidden="true"
|
||||
className="opacity-100"
|
||||
/>
|
||||
|
||||
<MaleGenitaliaIcon
|
||||
x="35"
|
||||
y="25"
|
||||
width="30"
|
||||
height="54"
|
||||
aria-hidden="true"
|
||||
className="opacity-100"
|
||||
/>
|
||||
</svg>
|
||||
<g transform="translate(0,158) scale(0.1,-0.1)" fill="currentColor" stroke="none">
|
||||
<path d="M355 1308 c-54 -168 -88 -256 -140 -363 -35 -71 -71 -157 -80 -190 -41 -159 4 -323 124 -448 90 -95 196 -145 326 -152 39 -2 73 -4 78 -4 4 -1 7 -25 7 -53 0 -29 3 -63 6 -75 l6 -23 368 0 368 0 12 30 c8 19 10 50 7 82 l-6 51 62 -6 c127 -13 265 36 365 128 180 166 209 398 79 635 -42 77 -67 137 -117 285 -33 99 -39 110 -61 110 -16 0 -25 -6 -27 -20 -5 -33 75 -251 143 -391 75 -154 93 -230 76 -325 -24 -137 -117 -262 -240 -322 -61 -30 -74 -32 -166 -32 -130 0 -191 23 -281 107 -109 102 -159 241 -134 371 7 34 13 73 15 87 4 36 20 70 49 109 14 19 26 43 26 53 0 26 -59 85 -102 103 -37 15 -38 15 -38 -7 0 -13 -4 -29 -9 -37 -13 -21 -41 -5 -41 24 0 50 -79 21 -126 -47 l-26 -37 47 -48 c77 -80 90 -120 89 -283 -1 -40 -7 -77 -18 -97 -9 -18 -16 -35 -16 -38 1 -25 -54 -106 -105 -154 -89 -83 -150 -106 -280 -106 -91 0 -106 3 -165 31 -168 80 -261 240 -247 424 5 67 13 89 77 218 77 157 158 378 147 405 -7 20 -46 23 -52 5z"/>
|
||||
<path d="M506 579 c-20 -16 -26 -29 -26 -57 1 -43 16 -67 53 -81 78 -30 142 75 82 134 -30 31 -73 32 -109 4z"/>
|
||||
<path d="M1525 575 c-74 -73 35 -186 110 -115 52 49 16 140 -55 140 -19 0 -40 -9 -55 -25z"/>
|
||||
</g>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1999,7 +1999,8 @@ export default function Player({
|
||||
className={cn(
|
||||
'relative w-full h-full player-video-frame',
|
||||
miniDesktop && 'vjs-mini',
|
||||
expanded && !isLive && 'vjs-expanded-player'
|
||||
expanded && !isLive && 'vjs-expanded-player',
|
||||
!expanded && !miniDesktop && !isLive && 'vjs-mobile-mini-player'
|
||||
)}
|
||||
style={{ ['--vjs-controlbar-h' as any]: `${controlBarH}px` }}
|
||||
>
|
||||
@ -2817,6 +2818,39 @@ export default function Player({
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
/* Mobiler MiniPlayer: Zeit ebenfalls anzeigen */
|
||||
.vjs-mobile-mini-player .video-js .vjs-current-time,
|
||||
.vjs-mobile-mini-player .video-js .vjs-time-divider,
|
||||
.vjs-mobile-mini-player .video-js .vjs-duration {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
.vjs-mobile-mini-player .video-js .vjs-time-control {
|
||||
min-width: auto !important;
|
||||
padding-left: 0.25em !important;
|
||||
padding-right: 0.25em !important;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.vjs-mobile-mini-player .video-js .vjs-current-time-display,
|
||||
.vjs-mobile-mini-player .video-js .vjs-duration-display {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vjs-mobile-mini-player .video-js .vjs-time-divider {
|
||||
padding-left: 0.1em !important;
|
||||
padding-right: 0.1em !important;
|
||||
}
|
||||
|
||||
.vjs-mobile-mini-player .video-js .vjs-skip-backward-10,
|
||||
.vjs-mobile-mini-player .video-js .vjs-skip-forward-10,
|
||||
.vjs-mobile-mini-player .video-js .vjs-playback-rate {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vjs-expanded-player .video-js .vjs-time-control {
|
||||
min-width: auto !important;
|
||||
padding-left: 0.35em !important;
|
||||
|
||||
@ -1072,7 +1072,6 @@ function segmentKindFromText(value: unknown): SegmentVisualKind {
|
||||
clean.includes('prone_bone') ||
|
||||
clean.includes('standing') ||
|
||||
clean.includes('spooning') ||
|
||||
clean.includes('sitting') ||
|
||||
clean.includes('facesitting') ||
|
||||
clean.includes('handjob') ||
|
||||
clean.includes('blowjob') ||
|
||||
|
||||
@ -42,17 +42,11 @@ export type SwipeCardProps = {
|
||||
onTap?: () => void
|
||||
onSwipeLeft: () => boolean | void | Promise<boolean | void>
|
||||
onSwipeRight: () => boolean | void | Promise<boolean | void>
|
||||
onSwipeUp?: () => boolean | void | Promise<boolean | void>
|
||||
onSwipeDown?: () => boolean | void | Promise<boolean | void>
|
||||
className?: string
|
||||
leftAction?: SwipeAction
|
||||
rightAction?: SwipeAction
|
||||
upAction?: SwipeAction
|
||||
downAction?: SwipeAction
|
||||
thresholdPx?: number
|
||||
thresholdRatio?: number
|
||||
verticalThresholdPx?: number
|
||||
verticalThresholdRatio?: number
|
||||
snapMs?: number
|
||||
commitMs?: number
|
||||
ignoreFromBottomPx?: number
|
||||
@ -71,8 +65,6 @@ export type SwipeCardProps = {
|
||||
export type SwipeCardHandle = {
|
||||
swipeLeft: (opts?: { runAction?: boolean }) => Promise<boolean>
|
||||
swipeRight: (opts?: { runAction?: boolean }) => Promise<boolean>
|
||||
swipeUp: (opts?: { runAction?: boolean }) => Promise<boolean>
|
||||
swipeDown: (opts?: { runAction?: boolean }) => Promise<boolean>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
@ -82,9 +74,7 @@ const FLICK_VELOCITY = 0.55
|
||||
const MAX_OVERSWIPE_PX = 64
|
||||
const ROTATE_MAX_DEG = 5
|
||||
const SCALE_MIN = 0.992
|
||||
type CommitDir = 'left' | 'right' | 'up' | 'down'
|
||||
|
||||
const VERTICAL_FLICK_VELOCITY = 0.55
|
||||
type CommitDir = 'left' | 'right'
|
||||
|
||||
function clamp(v: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, v))
|
||||
@ -112,17 +102,11 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
onTap,
|
||||
onSwipeLeft,
|
||||
onSwipeRight,
|
||||
onSwipeUp,
|
||||
onSwipeDown,
|
||||
className,
|
||||
leftAction,
|
||||
rightAction,
|
||||
upAction = { label: 'Öffnen' },
|
||||
downAction = { label: 'Weiter' },
|
||||
thresholdPx = 140,
|
||||
thresholdRatio = 0.28,
|
||||
verticalThresholdPx = 110,
|
||||
verticalThresholdRatio = 0.24,
|
||||
ignoreFromBottomPx = 72,
|
||||
ignoreSelector = '[data-swipe-ignore]',
|
||||
snapMs = 220,
|
||||
@ -148,22 +132,17 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
|
||||
const rafRef = useRef<number | null>(null)
|
||||
const dxRef = useRef(0)
|
||||
const dyRef = useRef(0)
|
||||
|
||||
const thresholdRef = useRef(0)
|
||||
const verticalThresholdRef = useRef(0)
|
||||
|
||||
const widthRef = useRef(360)
|
||||
const heightRef = useRef(520)
|
||||
|
||||
const tapTimerRef = useRef<number | null>(null)
|
||||
const lastTapRef = useRef<{ t: number; x: number; y: number } | null>(null)
|
||||
|
||||
const velocityXRef = useRef(0)
|
||||
const velocityYRef = useRef(0)
|
||||
|
||||
const lastMoveXRef = useRef(0)
|
||||
const lastMoveYRef = useRef(0)
|
||||
const lastMoveTRef = useRef(0)
|
||||
|
||||
const pointer = useRef<{
|
||||
@ -187,12 +166,10 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
})
|
||||
|
||||
const [dx, setDx] = useState(0)
|
||||
const [dy, setDy] = useState(0)
|
||||
const [armedDir, setArmedDir] = useState<null | CommitDir>(null)
|
||||
const [animMs, setAnimMs] = useState(0)
|
||||
|
||||
const hasVerticalSwipe = Boolean(onSwipeUp || onSwipeDown)
|
||||
const baseTouchAction = hasVerticalSwipe ? 'none' : 'pan-y'
|
||||
const baseTouchAction = 'pan-y'
|
||||
|
||||
const clearTapTimer = useCallback(() => {
|
||||
if (tapTimerRef.current != null) {
|
||||
@ -217,16 +194,14 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
onPressEnd?.()
|
||||
}, [clearPressTimer, onPressEnd])
|
||||
|
||||
const flushDrag = useCallback((nextDx: number, nextDy: number) => {
|
||||
const flushDx = useCallback((nextDx: number) => {
|
||||
dxRef.current = nextDx
|
||||
dyRef.current = nextDy
|
||||
|
||||
if (rafRef.current != null) return
|
||||
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
rafRef.current = null
|
||||
setDx(dxRef.current)
|
||||
setDy(dyRef.current)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@ -240,10 +215,8 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
}
|
||||
|
||||
dxRef.current = 0
|
||||
dyRef.current = 0
|
||||
|
||||
velocityXRef.current = 0
|
||||
velocityYRef.current = 0
|
||||
|
||||
if (cardRef.current) {
|
||||
cardRef.current.style.touchAction = baseTouchAction
|
||||
@ -251,7 +224,6 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
|
||||
setAnimMs(snapMs)
|
||||
setDx(0)
|
||||
setDy(0)
|
||||
setArmedDir(null)
|
||||
|
||||
window.setTimeout(() => {
|
||||
@ -271,25 +243,17 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
}
|
||||
|
||||
const w = cardRef.current?.offsetWidth || widthRef.current || 360
|
||||
const h = cardRef.current?.offsetHeight || heightRef.current || 520
|
||||
|
||||
const out =
|
||||
dir === 'right'
|
||||
? { x: w + 56, y: 0 }
|
||||
: dir === 'left'
|
||||
? { x: -(w + 56), y: 0 }
|
||||
: dir === 'up'
|
||||
? { x: 0, y: -(h * 0.72 + 56) }
|
||||
: { x: 0, y: h * 0.48 + 56 }
|
||||
? w + 56
|
||||
: -(w + 56)
|
||||
|
||||
setAnimMs(commitMs)
|
||||
setArmedDir(dir)
|
||||
|
||||
dxRef.current = out.x
|
||||
dyRef.current = out.y
|
||||
|
||||
setDx(out.x)
|
||||
setDy(out.y)
|
||||
dxRef.current = out
|
||||
setDx(out)
|
||||
|
||||
const animPromise = new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, commitMs)
|
||||
@ -300,20 +264,8 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
let ok: boolean | void = true
|
||||
|
||||
if (runAction) {
|
||||
const action =
|
||||
dir === 'right'
|
||||
? onSwipeRight
|
||||
: dir === 'left'
|
||||
? onSwipeLeft
|
||||
: dir === 'up'
|
||||
? onSwipeUp
|
||||
: onSwipeDown
|
||||
|
||||
if (!action) {
|
||||
ok = false
|
||||
} else {
|
||||
ok = await Promise.resolve(action()).catch(() => false)
|
||||
}
|
||||
const action = dir === 'right' ? onSwipeRight : onSwipeLeft
|
||||
ok = await Promise.resolve(action()).catch(() => false)
|
||||
}
|
||||
|
||||
if (ok === false) {
|
||||
@ -321,10 +273,7 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
setArmedDir(null)
|
||||
|
||||
dxRef.current = 0
|
||||
dyRef.current = 0
|
||||
|
||||
setDx(0)
|
||||
setDy(0)
|
||||
|
||||
window.setTimeout(() => setAnimMs(0), snapMs)
|
||||
return false
|
||||
@ -336,8 +285,6 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
commitMs,
|
||||
onSwipeLeft,
|
||||
onSwipeRight,
|
||||
onSwipeUp,
|
||||
onSwipeDown,
|
||||
snapMs,
|
||||
baseTouchAction,
|
||||
]
|
||||
@ -350,11 +297,9 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
}
|
||||
|
||||
dxRef.current = 0
|
||||
dyRef.current = 0
|
||||
|
||||
setAnimMs(0)
|
||||
setDx(0)
|
||||
setDy(0)
|
||||
setArmedDir(null)
|
||||
|
||||
try {
|
||||
@ -492,54 +437,28 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
() => ({
|
||||
swipeLeft: (opts) => commit('left', opts?.runAction ?? true),
|
||||
swipeRight: (opts) => commit('right', opts?.runAction ?? true),
|
||||
swipeUp: (opts) => commit('up', opts?.runAction ?? true),
|
||||
swipeDown: (opts) => commit('down', opts?.runAction ?? true),
|
||||
reset: () => reset(),
|
||||
}),
|
||||
[commit, reset]
|
||||
)
|
||||
|
||||
const absDx = Math.abs(dx)
|
||||
const absDy = Math.abs(dy)
|
||||
|
||||
const swipeDir: CommitDir | null =
|
||||
absDx === 0 && absDy === 0
|
||||
? null
|
||||
: absDy > absDx
|
||||
? dy > 0
|
||||
? 'down'
|
||||
: 'up'
|
||||
: dx > 0
|
||||
? 'right'
|
||||
: 'left'
|
||||
dx === 0 ? null : dx > 0 ? 'right' : 'left'
|
||||
|
||||
const activeThreshold =
|
||||
thresholdRef.current ||
|
||||
Math.min(thresholdPx, (cardRef.current?.offsetWidth || 360) * thresholdRatio)
|
||||
|
||||
const activeVerticalThreshold =
|
||||
verticalThresholdRef.current ||
|
||||
Math.min(
|
||||
verticalThresholdPx,
|
||||
(cardRef.current?.offsetHeight || 520) * verticalThresholdRatio
|
||||
)
|
||||
const reveal = clamp(absDx / Math.max(1, activeThreshold), 0, 1)
|
||||
|
||||
const reveal =
|
||||
swipeDir === 'up' || swipeDir === 'down'
|
||||
? clamp(absDy / Math.max(1, activeVerticalThreshold), 0, 1)
|
||||
: clamp(absDx / Math.max(1, activeThreshold), 0, 1)
|
||||
const revealSoft = clamp(absDx / Math.max(1, activeThreshold * 1.35), 0, 1)
|
||||
|
||||
const revealSoft =
|
||||
swipeDir === 'up' || swipeDir === 'down'
|
||||
? clamp(absDy / Math.max(1, activeVerticalThreshold * 1.35), 0, 1)
|
||||
: clamp(absDx / Math.max(1, activeThreshold * 1.35), 0, 1)
|
||||
|
||||
const tiltDeg = swipeDir === 'up' || swipeDir === 'down'
|
||||
? clamp(dx / 80, -2, 2)
|
||||
: clamp(dx / 34, -ROTATE_MAX_DEG, ROTATE_MAX_DEG)
|
||||
const tiltDeg = clamp(dx / 34, -ROTATE_MAX_DEG, ROTATE_MAX_DEG)
|
||||
|
||||
const dragScale =
|
||||
absDx === 0 && absDy === 0
|
||||
dx === 0
|
||||
? 1
|
||||
: Math.max(SCALE_MIN, 1 - revealSoft * 0.008)
|
||||
|
||||
@ -548,11 +467,7 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
? leftAction ?? { label: 'Löschen' }
|
||||
: armedDir === 'right'
|
||||
? rightAction ?? { label: 'Behalten' }
|
||||
: armedDir === 'up'
|
||||
? upAction
|
||||
: armedDir === 'down'
|
||||
? downAction
|
||||
: null
|
||||
: null
|
||||
|
||||
return (
|
||||
<div ref={outerRef} className={cn('relative isolate overflow-visible rounded-lg', className)}>
|
||||
@ -561,17 +476,17 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
className="relative"
|
||||
style={{
|
||||
transform:
|
||||
dx !== 0 || dy !== 0
|
||||
? `translate3d(${dx}px,${dy}px,0) rotate(${tiltDeg}deg) scale(${dragScale})`
|
||||
: undefined,
|
||||
dx !== 0
|
||||
? `translate3d(${dx}px,0,0) rotate(${tiltDeg}deg) scale(${dragScale})`
|
||||
: undefined,
|
||||
transition: animMs
|
||||
? `transform ${animMs}ms cubic-bezier(0.22, 1, 0.36, 1)`
|
||||
: undefined,
|
||||
touchAction: baseTouchAction,
|
||||
willChange: dx !== 0 || dy !== 0 ? 'transform' : undefined,
|
||||
borderRadius: dx !== 0 || dy !== 0 ? '12px' : undefined,
|
||||
willChange: dx !== 0 ? 'transform' : undefined,
|
||||
borderRadius: dx !== 0 ? '12px' : undefined,
|
||||
filter:
|
||||
dx !== 0 || dy !== 0
|
||||
dx !== 0
|
||||
? `saturate(${1 + reveal * 0.06}) brightness(${1 + reveal * 0.015})`
|
||||
: undefined,
|
||||
}}
|
||||
@ -651,13 +566,10 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
}
|
||||
|
||||
const w = cardRef.current?.offsetWidth || 360
|
||||
const h = cardRef.current?.offsetHeight || 520
|
||||
|
||||
widthRef.current = w
|
||||
heightRef.current = h
|
||||
|
||||
thresholdRef.current = Math.min(thresholdPx, w * thresholdRatio)
|
||||
verticalThresholdRef.current = Math.min(verticalThresholdPx, h * verticalThresholdRatio)
|
||||
|
||||
pointer.current = {
|
||||
id: e.pointerId,
|
||||
@ -671,14 +583,11 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
}
|
||||
|
||||
lastMoveXRef.current = e.clientX
|
||||
lastMoveYRef.current = e.clientY
|
||||
lastMoveTRef.current = performance.now()
|
||||
|
||||
velocityXRef.current = 0
|
||||
velocityYRef.current = 0
|
||||
|
||||
dxRef.current = 0
|
||||
dyRef.current = 0
|
||||
}}
|
||||
onPointerMove={(e) => {
|
||||
if (!enabled || disabled) return
|
||||
@ -696,24 +605,19 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
|
||||
if (absY > absX * LOCK_RATIO) {
|
||||
clearPressTimer()
|
||||
|
||||
if (!hasVerticalSwipe) {
|
||||
pointer.current.axisLocked = 'y'
|
||||
pointer.current.id = null
|
||||
return
|
||||
}
|
||||
|
||||
pointer.current.axisLocked = 'y'
|
||||
} else if (absX > absY * LOCK_RATIO) {
|
||||
pointer.current.id = null
|
||||
return
|
||||
}
|
||||
|
||||
if (absX > absY * LOCK_RATIO) {
|
||||
pointer.current.axisLocked = 'x'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (pointer.current.axisLocked !== 'x' && pointer.current.axisLocked !== 'y') {
|
||||
return
|
||||
}
|
||||
if (pointer.current.axisLocked !== 'x') return
|
||||
|
||||
if (!pointer.current.dragging) {
|
||||
clearPressTimer()
|
||||
@ -733,67 +637,34 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
const dt = Math.max(1, now - lastMoveTRef.current)
|
||||
|
||||
velocityXRef.current = (e.clientX - lastMoveXRef.current) / dt
|
||||
velocityYRef.current = (e.clientY - lastMoveYRef.current) / dt
|
||||
|
||||
lastMoveXRef.current = e.clientX
|
||||
lastMoveYRef.current = e.clientY
|
||||
lastMoveTRef.current = now
|
||||
|
||||
if (pointer.current.axisLocked === 'x') {
|
||||
const next = rubberBand(ddx, widthRef.current)
|
||||
flushDrag(next, 0)
|
||||
const next = rubberBand(ddx, widthRef.current)
|
||||
flushDx(next)
|
||||
|
||||
const threshold = thresholdRef.current
|
||||
const nextDir =
|
||||
next > threshold
|
||||
? 'right'
|
||||
: next < -threshold
|
||||
? 'left'
|
||||
: null
|
||||
const threshold = thresholdRef.current
|
||||
const nextDir =
|
||||
next > threshold
|
||||
? 'right'
|
||||
: next < -threshold
|
||||
? 'left'
|
||||
: null
|
||||
|
||||
setArmedDir((prev) => {
|
||||
if (prev === nextDir) return prev
|
||||
setArmedDir((prev) => {
|
||||
if (prev === nextDir) return prev
|
||||
|
||||
if (nextDir) {
|
||||
try {
|
||||
navigator.vibrate?.(10)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (nextDir) {
|
||||
try {
|
||||
navigator.vibrate?.(10)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return nextDir
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (pointer.current.axisLocked === 'y') {
|
||||
const next = rubberBand(ddy, heightRef.current)
|
||||
flushDrag(0, next)
|
||||
|
||||
const threshold = verticalThresholdRef.current
|
||||
const nextDir =
|
||||
next > threshold && onSwipeDown
|
||||
? 'down'
|
||||
: next < -threshold && onSwipeUp
|
||||
? 'up'
|
||||
: null
|
||||
|
||||
setArmedDir((prev) => {
|
||||
if (prev === nextDir) return prev
|
||||
|
||||
if (nextDir) {
|
||||
try {
|
||||
navigator.vibrate?.(10)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return nextDir
|
||||
})
|
||||
}
|
||||
return nextDir
|
||||
})
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
if (!enabled || disabled) return
|
||||
@ -895,50 +766,21 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
}
|
||||
|
||||
const finalDx = dxRef.current
|
||||
const finalDy = dyRef.current
|
||||
|
||||
const absX = Math.abs(finalDx)
|
||||
const absY = Math.abs(finalDy)
|
||||
const abs = Math.abs(finalDx)
|
||||
|
||||
const vx = velocityXRef.current
|
||||
const vy = velocityYRef.current
|
||||
|
||||
const fastRight = vx > FLICK_VELOCITY
|
||||
const fastLeft = vx < -FLICK_VELOCITY
|
||||
const fastDown = vy > VERTICAL_FLICK_VELOCITY
|
||||
const fastUp = vy < -VERTICAL_FLICK_VELOCITY
|
||||
|
||||
dxRef.current = 0
|
||||
dyRef.current = 0
|
||||
|
||||
const verticalThreshold =
|
||||
verticalThresholdRef.current ||
|
||||
Math.min(verticalThresholdPx, heightRef.current * verticalThresholdRatio)
|
||||
|
||||
if (hasVerticalSwipe && absY > absX) {
|
||||
if (
|
||||
onSwipeDown &&
|
||||
(finalDy > verticalThreshold || (fastDown && absY >= verticalThreshold * 0.5))
|
||||
) {
|
||||
void commit('down', true)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
onSwipeUp &&
|
||||
(finalDy < -verticalThreshold || (fastUp && absY >= verticalThreshold * 0.5))
|
||||
) {
|
||||
void commit('up', true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (finalDx > threshold || (fastRight && absX >= threshold * 0.5)) {
|
||||
if (finalDx > threshold || (fastRight && abs >= threshold * 0.5)) {
|
||||
void commit('right', true)
|
||||
return
|
||||
}
|
||||
|
||||
if (finalDx < -threshold || (fastLeft && absX >= threshold * 0.5)) {
|
||||
if (finalDx < -threshold || (fastLeft && abs >= threshold * 0.5)) {
|
||||
void commit('left', true)
|
||||
return
|
||||
}
|
||||
@ -975,10 +817,8 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
}
|
||||
|
||||
dxRef.current = 0
|
||||
dyRef.current = 0
|
||||
|
||||
velocityXRef.current = 0
|
||||
velocityYRef.current = 0
|
||||
|
||||
try {
|
||||
;(e.currentTarget as HTMLElement).style.touchAction = baseTouchAction
|
||||
@ -1001,11 +841,7 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
? 'linear-gradient(90deg, rgba(16,185,129,0.18) 0%, rgba(16,185,129,0.06) 42%, rgba(0,0,0,0) 100%)'
|
||||
: swipeDir === 'left'
|
||||
? 'linear-gradient(270deg, rgba(244,63,94,0.18) 0%, rgba(244,63,94,0.06) 42%, rgba(0,0,0,0) 100%)'
|
||||
: swipeDir === 'up'
|
||||
? 'linear-gradient(0deg, rgba(99,102,241,0.22) 0%, rgba(99,102,241,0.08) 46%, rgba(0,0,0,0) 100%)'
|
||||
: swipeDir === 'down'
|
||||
? 'linear-gradient(180deg, rgba(148,163,184,0.22) 0%, rgba(148,163,184,0.08) 46%, rgba(0,0,0,0) 100%)'
|
||||
: 'transparent',
|
||||
: 'transparent',
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1018,11 +854,7 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
||||
? 'inset 0 0 0 1px rgba(16,185,129,0.42), inset 0 0 28px rgba(16,185,129,0.12)'
|
||||
: armedDir === 'left'
|
||||
? 'inset 0 0 0 1px rgba(244,63,94,0.42), inset 0 0 28px rgba(244,63,94,0.12)'
|
||||
: armedDir === 'up'
|
||||
? 'inset 0 0 0 1px rgba(99,102,241,0.44), inset 0 0 32px rgba(99,102,241,0.15)'
|
||||
: armedDir === 'down'
|
||||
? 'inset 0 0 0 1px rgba(148,163,184,0.42), inset 0 0 32px rgba(148,163,184,0.13)'
|
||||
: 'none',
|
||||
: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@ -5253,8 +5253,9 @@ export default function TrainingTab(props: {
|
||||
}}
|
||||
title={isDraft ? box.label : `${item.text} verschieben`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
role={isDraft || uiLocked ? undefined : 'button'}
|
||||
aria-disabled={Boolean(isDraft) || uiLocked}
|
||||
className={[
|
||||
'group/label flex h-5 max-w-full min-w-0 touch-none select-none items-center overflow-hidden rounded-full text-left',
|
||||
isSmallBox ? 'gap-0.5 px-1' : 'gap-1 pl-1.5 pr-0.5',
|
||||
@ -5276,7 +5277,6 @@ export default function TrainingTab(props: {
|
||||
].join(' '),
|
||||
isDraft ? 'cursor-default' : 'cursor-move',
|
||||
].join(' ')}
|
||||
disabled={Boolean(isDraft) || uiLocked}
|
||||
onPointerDown={(e) => {
|
||||
if (isDraft || uiLocked) return
|
||||
|
||||
@ -5349,20 +5349,20 @@ export default function TrainingTab(props: {
|
||||
) : null}
|
||||
|
||||
{!isDraft ? (
|
||||
<span
|
||||
<button
|
||||
data-box-trash="true"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
disabled={uiLocked}
|
||||
className={[
|
||||
'ml-0.5 flex h-4.5 w-4.5 shrink-0 cursor-pointer touch-none items-center justify-center rounded-full',
|
||||
'!bg-red-600 !text-white ring-1 ring-red-800/40 transition',
|
||||
'hover:!bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500/60',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'dark:!bg-red-500 dark:!text-white dark:ring-white/20 dark:hover:!bg-red-600',
|
||||
].join(' ')}
|
||||
title={`${box.label} löschen`}
|
||||
aria-label={`${box.label} löschen`}
|
||||
onPointerDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@ -5370,20 +5370,14 @@ export default function TrainingTab(props: {
|
||||
e.stopPropagation()
|
||||
removeBox(index)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
removeBox(index)
|
||||
}}
|
||||
>
|
||||
<TrashIcon
|
||||
className="h-3 w-3 shrink-0 !text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isDraft ? (
|
||||
|
||||
@ -860,6 +860,143 @@ function normalizeAiSegmentsFromHits(
|
||||
})
|
||||
}
|
||||
|
||||
type SegmentVisualKind = 'position' | 'toy' | 'clothing' | 'body' | 'person' | 'default'
|
||||
|
||||
function segmentVisualKindFromText(value: unknown): SegmentVisualKind {
|
||||
const clean = String(value ?? '').trim().toLowerCase()
|
||||
if (!clean) return 'default'
|
||||
|
||||
if (
|
||||
clean.includes('person_male') ||
|
||||
clean.includes('male_person') ||
|
||||
clean.includes('person_female') ||
|
||||
clean.includes('female_person') ||
|
||||
clean === 'person' ||
|
||||
clean.startsWith('person:')
|
||||
) {
|
||||
return 'person'
|
||||
}
|
||||
|
||||
if (
|
||||
clean.startsWith('position:') ||
|
||||
clean.includes('missionary') ||
|
||||
clean.includes('doggy') ||
|
||||
clean.includes('doggystyle') ||
|
||||
clean.includes('cowgirl') ||
|
||||
clean.includes('reverse cowgirl') ||
|
||||
clean.includes('reverse_cowgirl') ||
|
||||
clean.includes('cunnilingus') ||
|
||||
clean.includes('prone bone') ||
|
||||
clean.includes('prone_bone') ||
|
||||
clean.includes('standing') ||
|
||||
clean.includes('spooning') ||
|
||||
clean.includes('facesitting') ||
|
||||
clean.includes('handjob') ||
|
||||
clean.includes('blowjob') ||
|
||||
clean.includes('boobjob') ||
|
||||
clean.includes('boob job') ||
|
||||
clean.includes('boob_job') ||
|
||||
clean.includes('titjob') ||
|
||||
clean.includes('tit job') ||
|
||||
clean.includes('tit_job') ||
|
||||
clean.includes('fingering') ||
|
||||
clean.includes('toy play') ||
|
||||
clean.includes('toy_play') ||
|
||||
clean === '69'
|
||||
) {
|
||||
return 'position'
|
||||
}
|
||||
|
||||
if (
|
||||
clean.includes('dildo') ||
|
||||
clean.includes('vibrator') ||
|
||||
clean.includes('buttplug') ||
|
||||
clean.includes('strapon') ||
|
||||
clean.includes('toy')
|
||||
) {
|
||||
return 'toy'
|
||||
}
|
||||
|
||||
if (
|
||||
clean.includes('lingerie') ||
|
||||
clean.includes('bikini') ||
|
||||
clean.includes('bra') ||
|
||||
clean.includes('bh') ||
|
||||
clean.includes('panties') ||
|
||||
clean.includes('slip') ||
|
||||
clean.includes('stocking') ||
|
||||
clean.includes('strümpfe') ||
|
||||
clean.includes('skirt') ||
|
||||
clean.includes('rock') ||
|
||||
clean.includes('dress') ||
|
||||
clean.includes('kleid') ||
|
||||
clean.includes('heels')
|
||||
) {
|
||||
return 'clothing'
|
||||
}
|
||||
|
||||
if (
|
||||
clean.includes('vagina') ||
|
||||
clean.includes('vulva') ||
|
||||
clean.includes('pussy') ||
|
||||
clean.includes('penis') ||
|
||||
clean.includes('anus') ||
|
||||
clean.includes('ass') ||
|
||||
clean.includes('hintern') ||
|
||||
clean.includes('breasts') ||
|
||||
clean.includes('brüste') ||
|
||||
clean.includes('buttocks')
|
||||
) {
|
||||
return 'body'
|
||||
}
|
||||
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function segmentVisualKind(seg: Segment): SegmentVisualKind {
|
||||
const rawLabel = String(seg.label || '').trim()
|
||||
const prettyLabel = getSegmentLabelText(rawLabel)
|
||||
|
||||
const values = [rawLabel, prettyLabel]
|
||||
|
||||
for (const value of values) {
|
||||
const kind = segmentVisualKindFromText(value)
|
||||
if (kind !== 'default') return kind
|
||||
}
|
||||
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function reindexSegments(input: Segment[]): Segment[] {
|
||||
return input.map((seg, idx) => ({
|
||||
...seg,
|
||||
index: idx + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
function filterSegmentsLikeRatingOverlay(input: Segment[]): Segment[] {
|
||||
const withoutPeople = input.filter((seg) => segmentVisualKind(seg) !== 'person')
|
||||
|
||||
const positionSegments = withoutPeople.filter((seg) => (
|
||||
segmentVisualKind(seg) === 'position'
|
||||
))
|
||||
|
||||
if (positionSegments.length > 0) {
|
||||
return reindexSegments(positionSegments)
|
||||
}
|
||||
|
||||
const bodyOrClothingSegments = withoutPeople.filter((seg) => {
|
||||
const kind = segmentVisualKind(seg)
|
||||
return kind === 'clothing' || kind === 'body'
|
||||
})
|
||||
|
||||
if (bodyOrClothingSegments.length > 0) {
|
||||
return reindexSegments(bodyOrClothingSegments)
|
||||
}
|
||||
|
||||
return reindexSegments(withoutPeople)
|
||||
}
|
||||
|
||||
function shouldAutoSelectAiHit(hit: {
|
||||
label: string
|
||||
}): boolean {
|
||||
@ -1085,7 +1222,7 @@ export default function VideoSplitModal({
|
||||
mapAiTimes
|
||||
)
|
||||
|
||||
const metaAiSegments =
|
||||
const rawMetaAiSegments =
|
||||
metaAiSegmentsFromMeta.length > 0
|
||||
? metaAiSegmentsFromMeta
|
||||
: normalizeAiSegmentsFromHits(
|
||||
@ -1094,6 +1231,8 @@ export default function VideoSplitModal({
|
||||
mapAiTimes
|
||||
)
|
||||
|
||||
const metaAiSegments = filterSegmentsLikeRatingOverlay(rawMetaAiSegments)
|
||||
|
||||
if (!cancelled) {
|
||||
setMediaDuration(metaDuration > 0 ? metaDuration : 0)
|
||||
setAiHits(metaAiHits)
|
||||
@ -1610,6 +1749,31 @@ export default function VideoSplitModal({
|
||||
)
|
||||
}, [job?.output, anyJob?.startedAt, anyJob?.endedAt, anyJob?.createdAt, anyJob?.fileCreatedAt, anyJob?.ctime])
|
||||
|
||||
const showSplitProgress = Boolean(
|
||||
splitProgress &&
|
||||
(
|
||||
busy ||
|
||||
splitProgress.phase === 'done' ||
|
||||
splitProgress.phase === 'error'
|
||||
)
|
||||
)
|
||||
|
||||
const splitOverallPercent = Math.round(
|
||||
clamp(splitProgress?.overallProgress ?? 0, 0, 1) * 100
|
||||
)
|
||||
|
||||
const splitSegmentPercent = Math.round(
|
||||
clamp(splitProgress?.segmentProgress ?? 0, 0, 1) * 100
|
||||
)
|
||||
|
||||
const splitCurrentLabel = splitProgress?.segment?.label
|
||||
? getSegmentLabelText(splitProgress.segment.label)
|
||||
: ''
|
||||
|
||||
const splitCurrentTime = splitProgress?.segment
|
||||
? `${formatTime(splitProgress.segment.start)} → ${formatTime(splitProgress.segment.end)}`
|
||||
: ''
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@ -1673,9 +1837,11 @@ export default function VideoSplitModal({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{segmentsToApply.length === 1
|
||||
? '1 Datei erzeugen'
|
||||
: `${segmentsToApply.length} Dateien erzeugen`}
|
||||
{busy
|
||||
? 'Wird erzeugt…'
|
||||
: segmentsToApply.length === 1
|
||||
? '1 Datei erzeugen'
|
||||
: `${segmentsToApply.length} Dateien erzeugen`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@ -2105,6 +2271,73 @@ export default function VideoSplitModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSplitProgress ? (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-3 rounded-xl px-3 py-3 ring-1',
|
||||
splitProgress?.phase === 'error'
|
||||
? 'bg-red-50 text-red-900 ring-red-200 dark:bg-red-500/10 dark:text-red-100 dark:ring-red-400/30'
|
||||
: splitProgress?.phase === 'done'
|
||||
? 'bg-emerald-50 text-emerald-900 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-100 dark:ring-emerald-400/30'
|
||||
: 'bg-indigo-50 text-indigo-950 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-100 dark:ring-indigo-400/30'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold">
|
||||
{splitProgress?.phase === 'done'
|
||||
? 'Split abgeschlossen'
|
||||
: splitProgress?.phase === 'error'
|
||||
? 'Split fehlgeschlagen'
|
||||
: 'Split läuft…'}
|
||||
</div>
|
||||
|
||||
<div className="mt-0.5 text-xs opacity-80">
|
||||
{splitProgress?.message || 'Bitte warten…'}
|
||||
</div>
|
||||
|
||||
{splitCurrentLabel ? (
|
||||
<div className="mt-1 truncate text-[11px] opacity-75">
|
||||
{splitCurrentLabel}
|
||||
{splitCurrentTime ? ` · ${splitCurrentTime}` : ''}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="text-sm font-black">
|
||||
{splitOverallPercent}%
|
||||
</div>
|
||||
|
||||
<div className="mt-0.5 text-[11px] opacity-70">
|
||||
{splitProgress?.completed ?? 0}/{splitProgress?.total ?? segmentsToApply.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 h-2 overflow-hidden rounded-full bg-black/10 dark:bg-white/15">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-[width] duration-150',
|
||||
splitProgress?.phase === 'error'
|
||||
? 'bg-red-500'
|
||||
: splitProgress?.phase === 'done'
|
||||
? 'bg-emerald-500'
|
||||
: 'bg-indigo-500'
|
||||
)}
|
||||
style={{ width: `${splitOverallPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{splitProgress?.phase === 'splitting' ? (
|
||||
<div className="mt-1 flex items-center justify-between text-[10px] opacity-70">
|
||||
<span>Aktuelles Segment</span>
|
||||
<span>{splitSegmentPercent}%</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-3">
|
||||
{displaySegmentGroups.map((group, groupIdx) => {
|
||||
const firstSeg = group.segments[0]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user