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 json
|
||||||
import os
|
import os
|
||||||
|
|||||||
@ -1509,7 +1509,7 @@ func buildHighlightHitsFromPrediction(pred TrainingPrediction, t float64) []anal
|
|||||||
continue
|
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 {
|
if sig.Group == "position" && segmentSeverityWeight(label) < 0.70 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"items": []
|
|
||||||
}
|
|
||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"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
|
var embeddedMLFiles embed.FS
|
||||||
|
|
||||||
func trainingEmbeddedMLDir() (string, error) {
|
func trainingEmbeddedMLDir() (string, error) {
|
||||||
@ -33,6 +33,10 @@ func trainingEmbeddedMLDir() (string, error) {
|
|||||||
for _, name := range append(files, optionalFiles...) {
|
for _, name := range append(files, optionalFiles...) {
|
||||||
srcPath := filepath.ToSlash(filepath.Join("ml", name))
|
srcPath := filepath.ToSlash(filepath.Join("ml", name))
|
||||||
|
|
||||||
|
if name == "detection_labels.json" {
|
||||||
|
srcPath = "generated/training/detection_labels.json"
|
||||||
|
}
|
||||||
|
|
||||||
b, err := embeddedMLFiles.ReadFile(srcPath)
|
b, err := embeddedMLFiles.ReadFile(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Pflichtdateien müssen vorhanden sein.
|
// 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
|
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 {
|
if set.Position < 0.60 && !set.HasBody && !set.HasObject && !set.HasClothing {
|
||||||
score = math.Min(score, 0.42)
|
score = math.Min(score, 0.42)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -464,9 +464,22 @@ func startAIServer(ctx context.Context) (*aiServerProcess, error) {
|
|||||||
|
|
||||||
trainingRoot, appBaseDir := findTrainingRootDir(scriptDir)
|
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"))
|
detectionLabelsPath := strings.TrimSpace(os.Getenv("DETECTION_LABELS_PATH"))
|
||||||
if detectionLabelsPath == "" {
|
if detectionLabelsPath == "" {
|
||||||
|
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")
|
detectionLabelsPath = filepath.Join(trainingRoot, "detection_labels.json")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
detectionLabelsPath = filepath.Clean(detectionLabelsPath)
|
detectionLabelsPath = filepath.Clean(detectionLabelsPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4301,7 +4301,7 @@ export default function App() {
|
|||||||
setSplitModalOpen(false)
|
setSplitModalOpen(false)
|
||||||
setSplitJob(null)
|
setSplitJob(null)
|
||||||
}}
|
}}
|
||||||
onApply={({ job, splits, segments, deleteOriginal }) => {
|
onApply={async ({ job, splits, segments, deleteOriginal, onProgress }) => {
|
||||||
const file = baseName(job.output || '')
|
const file = baseName(job.output || '')
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw new Error('Kein Dateiname vorhanden.')
|
throw new Error('Kein Dateiname vorhanden.')
|
||||||
@ -4338,13 +4338,18 @@ export default function App() {
|
|||||||
return segments[pos - 1] ?? null
|
return segments[pos - 1] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
const publish = (
|
type ModalSplitProgress = Parameters<NonNullable<typeof onProgress>>[0]
|
||||||
progress: Omit<BackgroundSplitProgress, 'sourceFile' | 'startedAtMs'>
|
|
||||||
) => {
|
const publish = (progress: ModalSplitProgress) => {
|
||||||
pushSplitProgress(file, progress)
|
pushSplitProgress(
|
||||||
|
file,
|
||||||
|
progress as Omit<BackgroundSplitProgress, 'sourceFile' | 'startedAtMs'>
|
||||||
|
)
|
||||||
|
|
||||||
|
onProgress?.(progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
return await (async () => {
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
let finalData: any = null
|
let finalData: any = null
|
||||||
let total = totalFallback
|
let total = totalFallback
|
||||||
@ -4562,6 +4567,7 @@ export default function App() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
notify.error('Split fehlgeschlagen', `${file} — ${message}`)
|
notify.error('Split fehlgeschlagen', `${file} — ${message}`)
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -20,7 +20,6 @@ export const AI_POSITION_LABELS = [
|
|||||||
'standing',
|
'standing',
|
||||||
'standing_doggy',
|
'standing_doggy',
|
||||||
'spooning',
|
'spooning',
|
||||||
'sitting',
|
|
||||||
'facesitting',
|
'facesitting',
|
||||||
'handjob',
|
'handjob',
|
||||||
'blowjob',
|
'blowjob',
|
||||||
@ -121,7 +120,6 @@ export const AI_LABEL_FALLBACKS: Record<string, string> = {
|
|||||||
standing: 'Stehend',
|
standing: 'Stehend',
|
||||||
standing_doggy: 'Standing Doggy',
|
standing_doggy: 'Standing Doggy',
|
||||||
spooning: 'Spooning',
|
spooning: 'Spooning',
|
||||||
sitting: 'Sitzend',
|
|
||||||
facesitting: 'Facesitting',
|
facesitting: 'Facesitting',
|
||||||
handjob: 'Handjob',
|
handjob: 'Handjob',
|
||||||
blowjob: 'Blowjob',
|
blowjob: 'Blowjob',
|
||||||
|
|||||||
@ -1324,10 +1324,6 @@ function FinishedDownloadsCardsView({
|
|||||||
doubleTapMaxMovePx={48}
|
doubleTapMaxMovePx={48}
|
||||||
pressDelayMs={220}
|
pressDelayMs={220}
|
||||||
enablePressHold={canSpeedHold}
|
enablePressHold={canSpeedHold}
|
||||||
verticalThresholdPx={92}
|
|
||||||
verticalThresholdRatio={0.20}
|
|
||||||
upAction={{ label: 'Öffnen' }}
|
|
||||||
downAction={{ label: 'Weiter' }}
|
|
||||||
onDoubleTap={async () => {
|
onDoubleTap={async () => {
|
||||||
if (isHot) return
|
if (isHot) return
|
||||||
await onToggleHot?.(j)
|
await onToggleHot?.(j)
|
||||||
@ -1361,16 +1357,6 @@ function FinishedDownloadsCardsView({
|
|||||||
setMobileTopTeaserEnabled(false)
|
setMobileTopTeaserEnabled(false)
|
||||||
return await keepVideo(j)
|
return await keepVideo(j)
|
||||||
}}
|
}}
|
||||||
onSwipeUp={() => {
|
|
||||||
setMobileTopTeaserEnabled(false)
|
|
||||||
openPlayer(j)
|
|
||||||
return false
|
|
||||||
}}
|
|
||||||
onSwipeDown={() => {
|
|
||||||
setMobileTopTeaserEnabled(false)
|
|
||||||
skipTopMobileCard()
|
|
||||||
return true
|
|
||||||
}}
|
|
||||||
onPressStart={(info) => {
|
onPressStart={(info) => {
|
||||||
if (info.pointerType === 'mouse' && info.button !== 0) return
|
if (info.pointerType === 'mouse' && info.button !== 0) return
|
||||||
if (!canSpeedHold) return
|
if (!canSpeedHold) return
|
||||||
@ -1397,10 +1383,6 @@ function FinishedDownloadsCardsView({
|
|||||||
|
|
||||||
{mobileTopRow ? (
|
{mobileTopRow ? (
|
||||||
<div className="mt-3 space-y-2">
|
<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 ? (
|
{mobileOrderedRows.length > 1 ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -64,38 +64,18 @@ export function FemaleBreastIcon(props: IconProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BoobjobIcon(props: IconProps) {
|
export function BoobjobIcon(props: IconProps) {
|
||||||
const { title, className, ...rest } = props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<IconBase
|
||||||
viewBox="0 0 100 100"
|
{...props}
|
||||||
fill="none"
|
className={iconClassName(props.className, 'origin-center scale-[1.12]')}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 211 158"
|
||||||
className={iconClassName(className, 'origin-center scale-[1.12]')}
|
|
||||||
aria-hidden={title ? undefined : true}
|
|
||||||
role={title ? 'img' : undefined}
|
|
||||||
{...rest}
|
|
||||||
>
|
>
|
||||||
{title ? <title>{title}</title> : null}
|
<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"/>
|
||||||
<FemaleBreastIcon
|
<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"/>
|
||||||
x="3"
|
<path d="M1525 575 c-74 -73 35 -186 110 -115 52 49 16 140 -55 140 -19 0 -40 -9 -55 -25z"/>
|
||||||
y="20"
|
</g>
|
||||||
width="94"
|
</IconBase>
|
||||||
height="58"
|
|
||||||
aria-hidden="true"
|
|
||||||
className="opacity-100"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MaleGenitaliaIcon
|
|
||||||
x="35"
|
|
||||||
y="25"
|
|
||||||
width="30"
|
|
||||||
height="54"
|
|
||||||
aria-hidden="true"
|
|
||||||
className="opacity-100"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1999,7 +1999,8 @@ export default function Player({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'relative w-full h-full player-video-frame',
|
'relative w-full h-full player-video-frame',
|
||||||
miniDesktop && 'vjs-mini',
|
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` }}
|
style={{ ['--vjs-controlbar-h' as any]: `${controlBarH}px` }}
|
||||||
>
|
>
|
||||||
@ -2817,6 +2818,39 @@ export default function Player({
|
|||||||
flex: 0 0 auto !important;
|
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 {
|
.vjs-expanded-player .video-js .vjs-time-control {
|
||||||
min-width: auto !important;
|
min-width: auto !important;
|
||||||
padding-left: 0.35em !important;
|
padding-left: 0.35em !important;
|
||||||
|
|||||||
@ -1072,7 +1072,6 @@ function segmentKindFromText(value: unknown): SegmentVisualKind {
|
|||||||
clean.includes('prone_bone') ||
|
clean.includes('prone_bone') ||
|
||||||
clean.includes('standing') ||
|
clean.includes('standing') ||
|
||||||
clean.includes('spooning') ||
|
clean.includes('spooning') ||
|
||||||
clean.includes('sitting') ||
|
|
||||||
clean.includes('facesitting') ||
|
clean.includes('facesitting') ||
|
||||||
clean.includes('handjob') ||
|
clean.includes('handjob') ||
|
||||||
clean.includes('blowjob') ||
|
clean.includes('blowjob') ||
|
||||||
|
|||||||
@ -42,17 +42,11 @@ export type SwipeCardProps = {
|
|||||||
onTap?: () => void
|
onTap?: () => void
|
||||||
onSwipeLeft: () => boolean | void | Promise<boolean | void>
|
onSwipeLeft: () => boolean | void | Promise<boolean | void>
|
||||||
onSwipeRight: () => 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
|
className?: string
|
||||||
leftAction?: SwipeAction
|
leftAction?: SwipeAction
|
||||||
rightAction?: SwipeAction
|
rightAction?: SwipeAction
|
||||||
upAction?: SwipeAction
|
|
||||||
downAction?: SwipeAction
|
|
||||||
thresholdPx?: number
|
thresholdPx?: number
|
||||||
thresholdRatio?: number
|
thresholdRatio?: number
|
||||||
verticalThresholdPx?: number
|
|
||||||
verticalThresholdRatio?: number
|
|
||||||
snapMs?: number
|
snapMs?: number
|
||||||
commitMs?: number
|
commitMs?: number
|
||||||
ignoreFromBottomPx?: number
|
ignoreFromBottomPx?: number
|
||||||
@ -71,8 +65,6 @@ export type SwipeCardProps = {
|
|||||||
export type SwipeCardHandle = {
|
export type SwipeCardHandle = {
|
||||||
swipeLeft: (opts?: { runAction?: boolean }) => Promise<boolean>
|
swipeLeft: (opts?: { runAction?: boolean }) => Promise<boolean>
|
||||||
swipeRight: (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
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,9 +74,7 @@ const FLICK_VELOCITY = 0.55
|
|||||||
const MAX_OVERSWIPE_PX = 64
|
const MAX_OVERSWIPE_PX = 64
|
||||||
const ROTATE_MAX_DEG = 5
|
const ROTATE_MAX_DEG = 5
|
||||||
const SCALE_MIN = 0.992
|
const SCALE_MIN = 0.992
|
||||||
type CommitDir = 'left' | 'right' | 'up' | 'down'
|
type CommitDir = 'left' | 'right'
|
||||||
|
|
||||||
const VERTICAL_FLICK_VELOCITY = 0.55
|
|
||||||
|
|
||||||
function clamp(v: number, min: number, max: number) {
|
function clamp(v: number, min: number, max: number) {
|
||||||
return Math.max(min, Math.min(max, v))
|
return Math.max(min, Math.min(max, v))
|
||||||
@ -112,17 +102,11 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
onTap,
|
onTap,
|
||||||
onSwipeLeft,
|
onSwipeLeft,
|
||||||
onSwipeRight,
|
onSwipeRight,
|
||||||
onSwipeUp,
|
|
||||||
onSwipeDown,
|
|
||||||
className,
|
className,
|
||||||
leftAction,
|
leftAction,
|
||||||
rightAction,
|
rightAction,
|
||||||
upAction = { label: 'Öffnen' },
|
|
||||||
downAction = { label: 'Weiter' },
|
|
||||||
thresholdPx = 140,
|
thresholdPx = 140,
|
||||||
thresholdRatio = 0.28,
|
thresholdRatio = 0.28,
|
||||||
verticalThresholdPx = 110,
|
|
||||||
verticalThresholdRatio = 0.24,
|
|
||||||
ignoreFromBottomPx = 72,
|
ignoreFromBottomPx = 72,
|
||||||
ignoreSelector = '[data-swipe-ignore]',
|
ignoreSelector = '[data-swipe-ignore]',
|
||||||
snapMs = 220,
|
snapMs = 220,
|
||||||
@ -148,22 +132,17 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
|
|
||||||
const rafRef = useRef<number | null>(null)
|
const rafRef = useRef<number | null>(null)
|
||||||
const dxRef = useRef(0)
|
const dxRef = useRef(0)
|
||||||
const dyRef = useRef(0)
|
|
||||||
|
|
||||||
const thresholdRef = useRef(0)
|
const thresholdRef = useRef(0)
|
||||||
const verticalThresholdRef = useRef(0)
|
|
||||||
|
|
||||||
const widthRef = useRef(360)
|
const widthRef = useRef(360)
|
||||||
const heightRef = useRef(520)
|
|
||||||
|
|
||||||
const tapTimerRef = useRef<number | null>(null)
|
const tapTimerRef = useRef<number | null>(null)
|
||||||
const lastTapRef = useRef<{ t: number; x: number; y: number } | null>(null)
|
const lastTapRef = useRef<{ t: number; x: number; y: number } | null>(null)
|
||||||
|
|
||||||
const velocityXRef = useRef(0)
|
const velocityXRef = useRef(0)
|
||||||
const velocityYRef = useRef(0)
|
|
||||||
|
|
||||||
const lastMoveXRef = useRef(0)
|
const lastMoveXRef = useRef(0)
|
||||||
const lastMoveYRef = useRef(0)
|
|
||||||
const lastMoveTRef = useRef(0)
|
const lastMoveTRef = useRef(0)
|
||||||
|
|
||||||
const pointer = useRef<{
|
const pointer = useRef<{
|
||||||
@ -187,12 +166,10 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [dx, setDx] = useState(0)
|
const [dx, setDx] = useState(0)
|
||||||
const [dy, setDy] = useState(0)
|
|
||||||
const [armedDir, setArmedDir] = useState<null | CommitDir>(null)
|
const [armedDir, setArmedDir] = useState<null | CommitDir>(null)
|
||||||
const [animMs, setAnimMs] = useState(0)
|
const [animMs, setAnimMs] = useState(0)
|
||||||
|
|
||||||
const hasVerticalSwipe = Boolean(onSwipeUp || onSwipeDown)
|
const baseTouchAction = 'pan-y'
|
||||||
const baseTouchAction = hasVerticalSwipe ? 'none' : 'pan-y'
|
|
||||||
|
|
||||||
const clearTapTimer = useCallback(() => {
|
const clearTapTimer = useCallback(() => {
|
||||||
if (tapTimerRef.current != null) {
|
if (tapTimerRef.current != null) {
|
||||||
@ -217,16 +194,14 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
onPressEnd?.()
|
onPressEnd?.()
|
||||||
}, [clearPressTimer, onPressEnd])
|
}, [clearPressTimer, onPressEnd])
|
||||||
|
|
||||||
const flushDrag = useCallback((nextDx: number, nextDy: number) => {
|
const flushDx = useCallback((nextDx: number) => {
|
||||||
dxRef.current = nextDx
|
dxRef.current = nextDx
|
||||||
dyRef.current = nextDy
|
|
||||||
|
|
||||||
if (rafRef.current != null) return
|
if (rafRef.current != null) return
|
||||||
|
|
||||||
rafRef.current = requestAnimationFrame(() => {
|
rafRef.current = requestAnimationFrame(() => {
|
||||||
rafRef.current = null
|
rafRef.current = null
|
||||||
setDx(dxRef.current)
|
setDx(dxRef.current)
|
||||||
setDy(dyRef.current)
|
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -240,10 +215,8 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
dxRef.current = 0
|
dxRef.current = 0
|
||||||
dyRef.current = 0
|
|
||||||
|
|
||||||
velocityXRef.current = 0
|
velocityXRef.current = 0
|
||||||
velocityYRef.current = 0
|
|
||||||
|
|
||||||
if (cardRef.current) {
|
if (cardRef.current) {
|
||||||
cardRef.current.style.touchAction = baseTouchAction
|
cardRef.current.style.touchAction = baseTouchAction
|
||||||
@ -251,7 +224,6 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
|
|
||||||
setAnimMs(snapMs)
|
setAnimMs(snapMs)
|
||||||
setDx(0)
|
setDx(0)
|
||||||
setDy(0)
|
|
||||||
setArmedDir(null)
|
setArmedDir(null)
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@ -271,25 +243,17 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
const w = cardRef.current?.offsetWidth || widthRef.current || 360
|
const w = cardRef.current?.offsetWidth || widthRef.current || 360
|
||||||
const h = cardRef.current?.offsetHeight || heightRef.current || 520
|
|
||||||
|
|
||||||
const out =
|
const out =
|
||||||
dir === 'right'
|
dir === 'right'
|
||||||
? { x: w + 56, y: 0 }
|
? w + 56
|
||||||
: dir === 'left'
|
: -(w + 56)
|
||||||
? { x: -(w + 56), y: 0 }
|
|
||||||
: dir === 'up'
|
|
||||||
? { x: 0, y: -(h * 0.72 + 56) }
|
|
||||||
: { x: 0, y: h * 0.48 + 56 }
|
|
||||||
|
|
||||||
setAnimMs(commitMs)
|
setAnimMs(commitMs)
|
||||||
setArmedDir(dir)
|
setArmedDir(dir)
|
||||||
|
|
||||||
dxRef.current = out.x
|
dxRef.current = out
|
||||||
dyRef.current = out.y
|
setDx(out)
|
||||||
|
|
||||||
setDx(out.x)
|
|
||||||
setDy(out.y)
|
|
||||||
|
|
||||||
const animPromise = new Promise<void>((resolve) => {
|
const animPromise = new Promise<void>((resolve) => {
|
||||||
window.setTimeout(resolve, commitMs)
|
window.setTimeout(resolve, commitMs)
|
||||||
@ -300,31 +264,16 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
let ok: boolean | void = true
|
let ok: boolean | void = true
|
||||||
|
|
||||||
if (runAction) {
|
if (runAction) {
|
||||||
const action =
|
const action = dir === 'right' ? onSwipeRight : onSwipeLeft
|
||||||
dir === 'right'
|
|
||||||
? onSwipeRight
|
|
||||||
: dir === 'left'
|
|
||||||
? onSwipeLeft
|
|
||||||
: dir === 'up'
|
|
||||||
? onSwipeUp
|
|
||||||
: onSwipeDown
|
|
||||||
|
|
||||||
if (!action) {
|
|
||||||
ok = false
|
|
||||||
} else {
|
|
||||||
ok = await Promise.resolve(action()).catch(() => false)
|
ok = await Promise.resolve(action()).catch(() => false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ok === false) {
|
if (ok === false) {
|
||||||
setAnimMs(snapMs)
|
setAnimMs(snapMs)
|
||||||
setArmedDir(null)
|
setArmedDir(null)
|
||||||
|
|
||||||
dxRef.current = 0
|
dxRef.current = 0
|
||||||
dyRef.current = 0
|
|
||||||
|
|
||||||
setDx(0)
|
setDx(0)
|
||||||
setDy(0)
|
|
||||||
|
|
||||||
window.setTimeout(() => setAnimMs(0), snapMs)
|
window.setTimeout(() => setAnimMs(0), snapMs)
|
||||||
return false
|
return false
|
||||||
@ -336,8 +285,6 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
commitMs,
|
commitMs,
|
||||||
onSwipeLeft,
|
onSwipeLeft,
|
||||||
onSwipeRight,
|
onSwipeRight,
|
||||||
onSwipeUp,
|
|
||||||
onSwipeDown,
|
|
||||||
snapMs,
|
snapMs,
|
||||||
baseTouchAction,
|
baseTouchAction,
|
||||||
]
|
]
|
||||||
@ -350,11 +297,9 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
dxRef.current = 0
|
dxRef.current = 0
|
||||||
dyRef.current = 0
|
|
||||||
|
|
||||||
setAnimMs(0)
|
setAnimMs(0)
|
||||||
setDx(0)
|
setDx(0)
|
||||||
setDy(0)
|
|
||||||
setArmedDir(null)
|
setArmedDir(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -492,54 +437,28 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
() => ({
|
() => ({
|
||||||
swipeLeft: (opts) => commit('left', opts?.runAction ?? true),
|
swipeLeft: (opts) => commit('left', opts?.runAction ?? true),
|
||||||
swipeRight: (opts) => commit('right', 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(),
|
reset: () => reset(),
|
||||||
}),
|
}),
|
||||||
[commit, reset]
|
[commit, reset]
|
||||||
)
|
)
|
||||||
|
|
||||||
const absDx = Math.abs(dx)
|
const absDx = Math.abs(dx)
|
||||||
const absDy = Math.abs(dy)
|
|
||||||
|
|
||||||
const swipeDir: CommitDir | null =
|
const swipeDir: CommitDir | null =
|
||||||
absDx === 0 && absDy === 0
|
dx === 0 ? null : dx > 0 ? 'right' : 'left'
|
||||||
? null
|
|
||||||
: absDy > absDx
|
|
||||||
? dy > 0
|
|
||||||
? 'down'
|
|
||||||
: 'up'
|
|
||||||
: dx > 0
|
|
||||||
? 'right'
|
|
||||||
: 'left'
|
|
||||||
|
|
||||||
const activeThreshold =
|
const activeThreshold =
|
||||||
thresholdRef.current ||
|
thresholdRef.current ||
|
||||||
Math.min(thresholdPx, (cardRef.current?.offsetWidth || 360) * thresholdRatio)
|
Math.min(thresholdPx, (cardRef.current?.offsetWidth || 360) * thresholdRatio)
|
||||||
|
|
||||||
const activeVerticalThreshold =
|
const reveal = clamp(absDx / Math.max(1, activeThreshold), 0, 1)
|
||||||
verticalThresholdRef.current ||
|
|
||||||
Math.min(
|
|
||||||
verticalThresholdPx,
|
|
||||||
(cardRef.current?.offsetHeight || 520) * verticalThresholdRatio
|
|
||||||
)
|
|
||||||
|
|
||||||
const reveal =
|
const revealSoft = clamp(absDx / Math.max(1, activeThreshold * 1.35), 0, 1)
|
||||||
swipeDir === 'up' || swipeDir === 'down'
|
|
||||||
? clamp(absDy / Math.max(1, activeVerticalThreshold), 0, 1)
|
|
||||||
: clamp(absDx / Math.max(1, activeThreshold), 0, 1)
|
|
||||||
|
|
||||||
const revealSoft =
|
const tiltDeg = clamp(dx / 34, -ROTATE_MAX_DEG, ROTATE_MAX_DEG)
|
||||||
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 dragScale =
|
const dragScale =
|
||||||
absDx === 0 && absDy === 0
|
dx === 0
|
||||||
? 1
|
? 1
|
||||||
: Math.max(SCALE_MIN, 1 - revealSoft * 0.008)
|
: Math.max(SCALE_MIN, 1 - revealSoft * 0.008)
|
||||||
|
|
||||||
@ -548,10 +467,6 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
? leftAction ?? { label: 'Löschen' }
|
? leftAction ?? { label: 'Löschen' }
|
||||||
: armedDir === 'right'
|
: armedDir === 'right'
|
||||||
? rightAction ?? { label: 'Behalten' }
|
? rightAction ?? { label: 'Behalten' }
|
||||||
: armedDir === 'up'
|
|
||||||
? upAction
|
|
||||||
: armedDir === 'down'
|
|
||||||
? downAction
|
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -561,17 +476,17 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
className="relative"
|
className="relative"
|
||||||
style={{
|
style={{
|
||||||
transform:
|
transform:
|
||||||
dx !== 0 || dy !== 0
|
dx !== 0
|
||||||
? `translate3d(${dx}px,${dy}px,0) rotate(${tiltDeg}deg) scale(${dragScale})`
|
? `translate3d(${dx}px,0,0) rotate(${tiltDeg}deg) scale(${dragScale})`
|
||||||
: undefined,
|
: undefined,
|
||||||
transition: animMs
|
transition: animMs
|
||||||
? `transform ${animMs}ms cubic-bezier(0.22, 1, 0.36, 1)`
|
? `transform ${animMs}ms cubic-bezier(0.22, 1, 0.36, 1)`
|
||||||
: undefined,
|
: undefined,
|
||||||
touchAction: baseTouchAction,
|
touchAction: baseTouchAction,
|
||||||
willChange: dx !== 0 || dy !== 0 ? 'transform' : undefined,
|
willChange: dx !== 0 ? 'transform' : undefined,
|
||||||
borderRadius: dx !== 0 || dy !== 0 ? '12px' : undefined,
|
borderRadius: dx !== 0 ? '12px' : undefined,
|
||||||
filter:
|
filter:
|
||||||
dx !== 0 || dy !== 0
|
dx !== 0
|
||||||
? `saturate(${1 + reveal * 0.06}) brightness(${1 + reveal * 0.015})`
|
? `saturate(${1 + reveal * 0.06}) brightness(${1 + reveal * 0.015})`
|
||||||
: undefined,
|
: undefined,
|
||||||
}}
|
}}
|
||||||
@ -651,13 +566,10 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
const w = cardRef.current?.offsetWidth || 360
|
const w = cardRef.current?.offsetWidth || 360
|
||||||
const h = cardRef.current?.offsetHeight || 520
|
|
||||||
|
|
||||||
widthRef.current = w
|
widthRef.current = w
|
||||||
heightRef.current = h
|
|
||||||
|
|
||||||
thresholdRef.current = Math.min(thresholdPx, w * thresholdRatio)
|
thresholdRef.current = Math.min(thresholdPx, w * thresholdRatio)
|
||||||
verticalThresholdRef.current = Math.min(verticalThresholdPx, h * verticalThresholdRatio)
|
|
||||||
|
|
||||||
pointer.current = {
|
pointer.current = {
|
||||||
id: e.pointerId,
|
id: e.pointerId,
|
||||||
@ -671,14 +583,11 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastMoveXRef.current = e.clientX
|
lastMoveXRef.current = e.clientX
|
||||||
lastMoveYRef.current = e.clientY
|
|
||||||
lastMoveTRef.current = performance.now()
|
lastMoveTRef.current = performance.now()
|
||||||
|
|
||||||
velocityXRef.current = 0
|
velocityXRef.current = 0
|
||||||
velocityYRef.current = 0
|
|
||||||
|
|
||||||
dxRef.current = 0
|
dxRef.current = 0
|
||||||
dyRef.current = 0
|
|
||||||
}}
|
}}
|
||||||
onPointerMove={(e) => {
|
onPointerMove={(e) => {
|
||||||
if (!enabled || disabled) return
|
if (!enabled || disabled) return
|
||||||
@ -696,24 +605,19 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
|
|
||||||
if (absY > absX * LOCK_RATIO) {
|
if (absY > absX * LOCK_RATIO) {
|
||||||
clearPressTimer()
|
clearPressTimer()
|
||||||
|
|
||||||
if (!hasVerticalSwipe) {
|
|
||||||
pointer.current.axisLocked = 'y'
|
pointer.current.axisLocked = 'y'
|
||||||
pointer.current.id = null
|
pointer.current.id = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pointer.current.axisLocked = 'y'
|
if (absX > absY * LOCK_RATIO) {
|
||||||
} else if (absX > absY * LOCK_RATIO) {
|
|
||||||
pointer.current.axisLocked = 'x'
|
pointer.current.axisLocked = 'x'
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pointer.current.axisLocked !== 'x' && pointer.current.axisLocked !== 'y') {
|
if (pointer.current.axisLocked !== 'x') return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pointer.current.dragging) {
|
if (!pointer.current.dragging) {
|
||||||
clearPressTimer()
|
clearPressTimer()
|
||||||
@ -733,15 +637,12 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
const dt = Math.max(1, now - lastMoveTRef.current)
|
const dt = Math.max(1, now - lastMoveTRef.current)
|
||||||
|
|
||||||
velocityXRef.current = (e.clientX - lastMoveXRef.current) / dt
|
velocityXRef.current = (e.clientX - lastMoveXRef.current) / dt
|
||||||
velocityYRef.current = (e.clientY - lastMoveYRef.current) / dt
|
|
||||||
|
|
||||||
lastMoveXRef.current = e.clientX
|
lastMoveXRef.current = e.clientX
|
||||||
lastMoveYRef.current = e.clientY
|
|
||||||
lastMoveTRef.current = now
|
lastMoveTRef.current = now
|
||||||
|
|
||||||
if (pointer.current.axisLocked === 'x') {
|
|
||||||
const next = rubberBand(ddx, widthRef.current)
|
const next = rubberBand(ddx, widthRef.current)
|
||||||
flushDrag(next, 0)
|
flushDx(next)
|
||||||
|
|
||||||
const threshold = thresholdRef.current
|
const threshold = thresholdRef.current
|
||||||
const nextDir =
|
const nextDir =
|
||||||
@ -764,36 +665,6 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
|
|
||||||
return nextDir
|
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onPointerUp={(e) => {
|
onPointerUp={(e) => {
|
||||||
if (!enabled || disabled) return
|
if (!enabled || disabled) return
|
||||||
@ -895,50 +766,21 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
const finalDx = dxRef.current
|
const finalDx = dxRef.current
|
||||||
const finalDy = dyRef.current
|
const abs = Math.abs(finalDx)
|
||||||
|
|
||||||
const absX = Math.abs(finalDx)
|
|
||||||
const absY = Math.abs(finalDy)
|
|
||||||
|
|
||||||
const vx = velocityXRef.current
|
const vx = velocityXRef.current
|
||||||
const vy = velocityYRef.current
|
|
||||||
|
|
||||||
const fastRight = vx > FLICK_VELOCITY
|
const fastRight = vx > FLICK_VELOCITY
|
||||||
const fastLeft = vx < -FLICK_VELOCITY
|
const fastLeft = vx < -FLICK_VELOCITY
|
||||||
const fastDown = vy > VERTICAL_FLICK_VELOCITY
|
|
||||||
const fastUp = vy < -VERTICAL_FLICK_VELOCITY
|
|
||||||
|
|
||||||
dxRef.current = 0
|
dxRef.current = 0
|
||||||
dyRef.current = 0
|
|
||||||
|
|
||||||
const verticalThreshold =
|
if (finalDx > threshold || (fastRight && abs >= threshold * 0.5)) {
|
||||||
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)) {
|
|
||||||
void commit('right', true)
|
void commit('right', true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalDx < -threshold || (fastLeft && absX >= threshold * 0.5)) {
|
if (finalDx < -threshold || (fastLeft && abs >= threshold * 0.5)) {
|
||||||
void commit('left', true)
|
void commit('left', true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -975,10 +817,8 @@ const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
dxRef.current = 0
|
dxRef.current = 0
|
||||||
dyRef.current = 0
|
|
||||||
|
|
||||||
velocityXRef.current = 0
|
velocityXRef.current = 0
|
||||||
velocityYRef.current = 0
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
;(e.currentTarget as HTMLElement).style.touchAction = baseTouchAction
|
;(e.currentTarget as HTMLElement).style.touchAction = baseTouchAction
|
||||||
@ -1001,10 +841,6 @@ 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%)'
|
? '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'
|
: 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%)'
|
? '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,10 +854,6 @@ 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)'
|
? 'inset 0 0 0 1px rgba(16,185,129,0.42), inset 0 0 28px rgba(16,185,129,0.12)'
|
||||||
: armedDir === 'left'
|
: armedDir === 'left'
|
||||||
? 'inset 0 0 0 1px rgba(244,63,94,0.42), inset 0 0 28px rgba(244,63,94,0.12)'
|
? '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`}
|
title={isDraft ? box.label : `${item.text} verschieben`}
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
type="button"
|
role={isDraft || uiLocked ? undefined : 'button'}
|
||||||
|
aria-disabled={Boolean(isDraft) || uiLocked}
|
||||||
className={[
|
className={[
|
||||||
'group/label flex h-5 max-w-full min-w-0 touch-none select-none items-center overflow-hidden rounded-full text-left',
|
'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',
|
isSmallBox ? 'gap-0.5 px-1' : 'gap-1 pl-1.5 pr-0.5',
|
||||||
@ -5276,7 +5277,6 @@ export default function TrainingTab(props: {
|
|||||||
].join(' '),
|
].join(' '),
|
||||||
isDraft ? 'cursor-default' : 'cursor-move',
|
isDraft ? 'cursor-default' : 'cursor-move',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
disabled={Boolean(isDraft) || uiLocked}
|
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
if (isDraft || uiLocked) return
|
if (isDraft || uiLocked) return
|
||||||
|
|
||||||
@ -5349,20 +5349,20 @@ export default function TrainingTab(props: {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!isDraft ? (
|
{!isDraft ? (
|
||||||
<span
|
<button
|
||||||
data-box-trash="true"
|
data-box-trash="true"
|
||||||
role="button"
|
type="button"
|
||||||
tabIndex={0}
|
disabled={uiLocked}
|
||||||
className={[
|
className={[
|
||||||
'ml-0.5 flex h-4.5 w-4.5 shrink-0 cursor-pointer touch-none items-center justify-center rounded-full',
|
'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',
|
'!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',
|
'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',
|
'dark:!bg-red-500 dark:!text-white dark:ring-white/20 dark:hover:!bg-red-600',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
title={`${box.label} löschen`}
|
title={`${box.label} löschen`}
|
||||||
aria-label={`${box.label} löschen`}
|
aria-label={`${box.label} löschen`}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -5370,20 +5370,14 @@ export default function TrainingTab(props: {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
removeBox(index)
|
removeBox(index)
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key !== 'Enter' && e.key !== ' ') return
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
removeBox(index)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
className="h-3 w-3 shrink-0 !text-white"
|
className="h-3 w-3 shrink-0 !text-white"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</button>
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isDraft ? (
|
{!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: {
|
function shouldAutoSelectAiHit(hit: {
|
||||||
label: string
|
label: string
|
||||||
}): boolean {
|
}): boolean {
|
||||||
@ -1085,7 +1222,7 @@ export default function VideoSplitModal({
|
|||||||
mapAiTimes
|
mapAiTimes
|
||||||
)
|
)
|
||||||
|
|
||||||
const metaAiSegments =
|
const rawMetaAiSegments =
|
||||||
metaAiSegmentsFromMeta.length > 0
|
metaAiSegmentsFromMeta.length > 0
|
||||||
? metaAiSegmentsFromMeta
|
? metaAiSegmentsFromMeta
|
||||||
: normalizeAiSegmentsFromHits(
|
: normalizeAiSegmentsFromHits(
|
||||||
@ -1094,6 +1231,8 @@ export default function VideoSplitModal({
|
|||||||
mapAiTimes
|
mapAiTimes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const metaAiSegments = filterSegmentsLikeRatingOverlay(rawMetaAiSegments)
|
||||||
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setMediaDuration(metaDuration > 0 ? metaDuration : 0)
|
setMediaDuration(metaDuration > 0 ? metaDuration : 0)
|
||||||
setAiHits(metaAiHits)
|
setAiHits(metaAiHits)
|
||||||
@ -1610,6 +1749,31 @@ export default function VideoSplitModal({
|
|||||||
)
|
)
|
||||||
}, [job?.output, anyJob?.startedAt, anyJob?.endedAt, anyJob?.createdAt, anyJob?.fileCreatedAt, anyJob?.ctime])
|
}, [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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
@ -1673,7 +1837,9 @@ export default function VideoSplitModal({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{segmentsToApply.length === 1
|
{busy
|
||||||
|
? 'Wird erzeugt…'
|
||||||
|
: segmentsToApply.length === 1
|
||||||
? '1 Datei erzeugen'
|
? '1 Datei erzeugen'
|
||||||
: `${segmentsToApply.length} Dateien erzeugen`}
|
: `${segmentsToApply.length} Dateien erzeugen`}
|
||||||
</Button>
|
</Button>
|
||||||
@ -2105,6 +2271,73 @@ export default function VideoSplitModal({
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="space-y-3">
|
||||||
{displaySegmentGroups.map((group, groupIdx) => {
|
{displaySegmentGroups.map((group, groupIdx) => {
|
||||||
const firstSeg = group.segments[0]
|
const firstSeg = group.segments[0]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user