goon/mobile/src/SceneActionsContext.tsx
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

171 lines
5.3 KiB
TypeScript

/**
* SceneActions, long-press akcje na kafelkach scen (bug-report 5a6844db).
* Zastępuje martwy animated-preview gesture. Long-press → menu:
* - Ukryj scenę → POST /scenes/{id}/hide (playback dead, znika z list)
* - Oznacz duplikat → wejście w tryb wyboru; następny tap na inną scenę scala
* (merge długo-naciśniętej W wybraną). Banner na dole prowadzi przez wybór.
*
* Context bo SceneTile jest współdzielony przez 5 ekranów, jeden provider daje
* tryb selekcji działający wszędzie bez per-screen wiringu.
*/
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';
import { useClient } from './ClientContext';
import { theme } from './theme';
import type { SceneOut } from './types';
interface SceneActionsCtx {
pendingDuplicate: SceneOut | null;
isSelecting: boolean;
/** Otwórz menu akcji dla sceny (long-press). */
openActions: (scene: SceneOut) => void;
/** W trybie selekcji: wybierz tę scenę jako oryginał i scal w nią duplikat. */
pickDuplicateTarget: (target: SceneOut) => void;
cancelDuplicate: () => void;
}
const Ctx = React.createContext<SceneActionsCtx | null>(null);
export function useSceneActions(): SceneActionsCtx {
const c = React.useContext(Ctx);
if (!c) throw new Error('useSceneActions used outside SceneActionsProvider');
return c;
}
const SCENE_LIST_KEYS = ['scenes', 'performer-scenes', 'studio-scenes', 'tag-scenes', 'site-scenes'];
export function SceneActionsProvider({ children }: { children: React.ReactNode }) {
const client = useClient();
const queryClient = useQueryClient();
const [pendingDuplicate, setPending] = React.useState<SceneOut | null>(null);
const invalidate = React.useCallback(() => {
for (const k of SCENE_LIST_KEYS) {
queryClient.invalidateQueries({ queryKey: [k] });
}
}, [queryClient]);
const hide = React.useCallback(
(scene: SceneOut) => {
Alert.alert('Hide scene?', scene.title, [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Hide',
style: 'destructive',
onPress: async () => {
try {
await client.hideScene(scene.id);
invalidate();
} catch (e: any) {
Alert.alert('Failed to hide', e?.message || 'unknown error');
}
},
},
]);
},
[client, invalidate],
);
const openActions = React.useCallback(
(scene: SceneOut) => {
if (pendingDuplicate) return; // w trakcie wyboru duplikatu, ignoruj
Alert.alert(scene.title, 'Scene actions', [
{ text: 'Hide scene', style: 'destructive', onPress: () => hide(scene) },
{ text: 'Mark as duplicate', onPress: () => setPending(scene) },
{ text: 'Cancel', style: 'cancel' },
]);
},
[pendingDuplicate, hide],
);
const pickDuplicateTarget = React.useCallback(
(target: SceneOut) => {
const dup = pendingDuplicate;
if (!dup || target.id === dup.id) {
setPending(null);
return;
}
Alert.alert(
'Merge duplicate?',
`"${dup.title}"\n\nmerge into\n\n"${target.title}"`,
[
{ text: 'Cancel', style: 'cancel', onPress: () => setPending(null) },
{
text: 'Merge',
onPress: async () => {
try {
// keep = wybrany oryginał (target), drop = długo-naciśnięty duplikat (dup)
await client.mergeDuplicateScene(target.id, dup.id);
invalidate();
} catch (e: any) {
Alert.alert('Merge failed', e?.message || 'unknown error');
} finally {
setPending(null);
}
},
},
],
);
},
[pendingDuplicate, client, invalidate],
);
const cancelDuplicate = React.useCallback(() => setPending(null), []);
const value = React.useMemo(
() => ({
pendingDuplicate,
isSelecting: !!pendingDuplicate,
openActions,
pickDuplicateTarget,
cancelDuplicate,
}),
[pendingDuplicate, openActions, pickDuplicateTarget, cancelDuplicate],
);
return (
<Ctx.Provider value={value}>
{children}
{pendingDuplicate ? (
<View style={styles.banner} pointerEvents="box-none">
<View style={styles.bannerInner}>
<Text style={styles.bannerText} numberOfLines={2}>
Pick the original, tap the scene to merge "{pendingDuplicate.title}" into
</Text>
<Pressable onPress={cancelDuplicate} style={styles.bannerCancel} hitSlop={8}>
<Text style={styles.bannerCancelText}>Cancel</Text>
</Pressable>
</View>
</View>
) : null}
</Ctx.Provider>
);
}
const styles = StyleSheet.create({
banner: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
padding: 10,
},
bannerInner: {
backgroundColor: theme.accent,
borderRadius: 12,
padding: 12,
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
bannerText: { color: theme.fg, fontSize: 13, fontWeight: '600', flex: 1 },
bannerCancel: {
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: 8,
paddingVertical: 6,
paddingHorizontal: 12,
},
bannerCancelText: { color: theme.fg, fontWeight: '700', fontSize: 13 },
});