feat: wylaczalny auto-rotate w playerze + '+N' liczy takze filmy
Some checks are pending
Backend tests / test (push) Waiting to run
Some checks are pending
Backend tests / test (push) Waiting to run
1) Auto-rotate (bug-report 55da1c3f: 'I can't turn off auto rotate. Which is a problem when I put the phone on a flat surface'). Player robil bezwarunkowy unlockAsync, wiec telefon lezacy plasko skakal miedzy orientacjami. Nowa preferencja goon.player_auto_rotate (default true = dotychczasowe zachowanie), przelacznik w Settings. Off = player zostaje w pionie, ale przycisk pelnego ekranu dalej wchodzi w poziom recznie, wiec nic nie ginie. 2) '+N nowych' na ulubionym performerze/studiu liczylo TYLKO sceny (user-request 2026-07-28). Dochodzi _new_movie_counts: filmy z zywym playbackiem, created_at > last_seen_at, po MoviePerformer / Movie.studio_id. Filmow jest o rzedy wielkosci mniej niz scen (brak tube-spamu), wiec bez okna top-N i odsiewu stub/backfill - te pojecia dotycza scen. Zweryfikowane: performer 1633 scen + 436 filmow = 2069, studio 648 + 95 = 743, /favorites 200 OK.
This commit is contained in:
parent
9af5fd4eb4
commit
90a63b1e77
6 changed files with 138 additions and 7 deletions
|
|
@ -27,7 +27,7 @@ from typing import Annotated
|
|||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import exists, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.device import get_device_id
|
||||
|
|
@ -128,6 +128,63 @@ def _new_counts(session: Session, device_id: str, *, kind: str) -> dict:
|
|||
return {gid_val: int(n) for gid_val, n in rows}
|
||||
|
||||
|
||||
def _new_movie_counts(session: Session, device_id: str, *, kind: str) -> dict:
|
||||
"""To samo co `_new_counts`, ale dla FILMÓW (user-request 2026-07-28: "+N" ma się
|
||||
pojawiać też gdy ulubiony performer/studio dostanie nowy film, nie tylko scenę).
|
||||
|
||||
Filtr widoczności jak w `list_movies`: film musi mieć żywy playback. Filmów jest
|
||||
o rzędy wielkości mniej niż scen (brak tube-spamu), więc nie potrzeba okna
|
||||
top-N ani odsiewu stub/backfill — te pojęcia dotyczą scen.
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
from app.models.movie import Movie, MoviePerformer
|
||||
from app.models.movie_playback_source import MoviePlaybackSource
|
||||
|
||||
live_movie = exists(
|
||||
select(1).where(
|
||||
MoviePlaybackSource.movie_id == Movie.id,
|
||||
MoviePlaybackSource.dead_at.is_(None),
|
||||
)
|
||||
)
|
||||
|
||||
if kind == "performer":
|
||||
base = (
|
||||
select(
|
||||
MoviePerformer.performer_id.label("gid"),
|
||||
func.count()
|
||||
.filter(Movie.created_at > FavoritePerformer.last_seen_at)
|
||||
.label("n"),
|
||||
)
|
||||
.select_from(FavoritePerformer)
|
||||
.join(MoviePerformer, MoviePerformer.performer_id == FavoritePerformer.performer_id)
|
||||
.join(Movie, Movie.id == MoviePerformer.movie_id)
|
||||
.where(FavoritePerformer.device_id == device_id, live_movie)
|
||||
.group_by(MoviePerformer.performer_id)
|
||||
)
|
||||
else:
|
||||
base = (
|
||||
select(
|
||||
Movie.studio_id.label("gid"),
|
||||
func.count().filter(Movie.created_at > FavoriteStudio.last_seen_at).label("n"),
|
||||
)
|
||||
.select_from(FavoriteStudio)
|
||||
.join(Movie, Movie.studio_id == FavoriteStudio.studio_id)
|
||||
.where(FavoriteStudio.device_id == device_id, live_movie)
|
||||
.group_by(Movie.studio_id)
|
||||
)
|
||||
|
||||
return {gid: int(n) for gid, n in session.execute(base).all() if n}
|
||||
|
||||
|
||||
def _merged_new_counts(session: Session, device_id: str, *, kind: str) -> dict:
|
||||
"""Sceny + filmy w jednym liczniku "+N" (badge nie rozróżnia typu treści)."""
|
||||
counts = _new_counts(session, device_id, kind=kind)
|
||||
for gid, n in _new_movie_counts(session, device_id, kind=kind).items():
|
||||
counts[gid] = counts.get(gid, 0) + n
|
||||
return counts
|
||||
|
||||
|
||||
class FavoriteOut(BaseModel):
|
||||
performer_id: uuid.UUID
|
||||
canonical_name: str
|
||||
|
|
@ -162,7 +219,7 @@ def list_favorites(
|
|||
# _job_refresh_taxonomy_counts) — ta sama definicja co przed (sceny z żywym
|
||||
# playback). Wcześniej grouped count z EXISTS playback per-request. Migracja 0019.
|
||||
scene_counts: dict = {perf.id: perf.scene_count for _, perf in rows}
|
||||
new_counts = _new_counts(session, device_id, kind="performer")
|
||||
new_counts = _merged_new_counts(session, device_id, kind="performer")
|
||||
|
||||
items: list[FavoriteOut] = []
|
||||
new_total = 0
|
||||
|
|
@ -277,7 +334,7 @@ def list_favorite_studios(
|
|||
|
||||
# scene_count: zdenormalizowany Studio.scene_count (refresh w tle, migracja 0019).
|
||||
scene_counts: dict = {st.id: st.scene_count for _, st in rows}
|
||||
new_counts = _new_counts(session, device_id, kind="studio")
|
||||
new_counts = _merged_new_counts(session, device_id, kind="studio")
|
||||
|
||||
items: list[FavoriteStudioOut] = []
|
||||
new_total = 0
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
|||
|
||||
import {
|
||||
DefaultQuality,
|
||||
getAutoRotate,
|
||||
getDefaultQuality,
|
||||
getGridColumns,
|
||||
setAutoRotate as persistAutoRotate,
|
||||
setDefaultQuality as persistDefaultQuality,
|
||||
setGridColumns as persistGridColumns,
|
||||
} from './storage';
|
||||
|
|
@ -17,6 +19,8 @@ interface Prefs {
|
|||
setGridColumns: (n: number) => void;
|
||||
defaultQuality: DefaultQuality;
|
||||
setDefaultQuality: (q: DefaultQuality) => void;
|
||||
autoRotate: boolean;
|
||||
setAutoRotate: (on: boolean) => void;
|
||||
}
|
||||
|
||||
const Ctx = createContext<Prefs | null>(null);
|
||||
|
|
@ -24,6 +28,7 @@ const Ctx = createContext<Prefs | null>(null);
|
|||
export function PreferencesProvider({ children }: { children: React.ReactNode }) {
|
||||
const [gridColumns, setCols] = useState(2);
|
||||
const [defaultQuality, setQuality] = useState<DefaultQuality>('auto');
|
||||
const [autoRotate, setRotate] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getGridColumns()
|
||||
|
|
@ -32,6 +37,9 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
|
|||
getDefaultQuality()
|
||||
.then(setQuality)
|
||||
.catch(() => {});
|
||||
getAutoRotate()
|
||||
.then(setRotate)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const setGridColumns = (n: number) => {
|
||||
|
|
@ -44,8 +52,22 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
|
|||
persistDefaultQuality(q).catch(() => {});
|
||||
};
|
||||
|
||||
const setAutoRotate = (on: boolean) => {
|
||||
setRotate(on);
|
||||
persistAutoRotate(on).catch(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<Ctx.Provider value={{ gridColumns, setGridColumns, defaultQuality, setDefaultQuality }}>
|
||||
<Ctx.Provider
|
||||
value={{
|
||||
gridColumns,
|
||||
setGridColumns,
|
||||
defaultQuality,
|
||||
setDefaultQuality,
|
||||
autoRotate,
|
||||
setAutoRotate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Ctx.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ export type ChangelogEntry = {
|
|||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
id: '2026-07-28',
|
||||
date: 'July 2026',
|
||||
items: [
|
||||
'Auto-rotate can now be turned off (Settings -> Auto-rotate), so the video stops flipping when the phone lies flat on a table. The fullscreen button still switches to landscape.',
|
||||
'The "+N new" badge on a favourite performer or studio now also counts new movies, not just scenes.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2026-07-26b',
|
||||
date: 'July 2026',
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export function AppLockSettingsScreen() {
|
|||
const [stage, setStage] = useState<Stage>('menu');
|
||||
const [newPin, setNewPin] = useState('');
|
||||
const [errorText, setErrorText] = useState<string | null>(null);
|
||||
const { gridColumns, setGridColumns, defaultQuality, setDefaultQuality } = usePreferences();
|
||||
const { gridColumns, setGridColumns, defaultQuality, setDefaultQuality, autoRotate, setAutoRotate } = usePreferences();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -376,6 +376,24 @@ export function AppLockSettingsScreen() {
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Auto-rotate</Text>
|
||||
<Text style={styles.hint}>
|
||||
Let the video follow your phone's rotation. Turn this off if the picture keeps
|
||||
flipping when the phone lies flat on a table. The fullscreen button still switches
|
||||
to landscape.
|
||||
</Text>
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.label}>Rotate with the phone</Text>
|
||||
<Text style={styles.hint}>
|
||||
{autoRotate ? 'Video follows the phone' : 'Video stays upright'}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch value={autoRotate} onValueChange={setAutoRotate} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Backup & sync</Text>
|
||||
<Text style={styles.hint}>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useVideoPlayer, VideoView, type VideoSource } from 'expo-video';
|
|||
import * as Clipboard from 'expo-clipboard';
|
||||
import { setLastPlayback } from '../lib/lastPlayback';
|
||||
import { allowScreenOff, keepScreenOn } from '../lib/keepAwake';
|
||||
import { usePreferences } from '../PreferencesContext';
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
|
|
@ -135,12 +136,20 @@ export function PlayerScreen() {
|
|||
const params = route.params as RouteParams;
|
||||
const mode = params.mode ?? detectMode(params.url);
|
||||
|
||||
// Auto-obrót: domyślnie odblokowany (oglądanie w poziomie). Gdy user go wyłączy
|
||||
// (Settings → Playback), player zostaje w pionie — telefon leżący płasko na stole
|
||||
// nie skacze między orientacjami (bug-report 55da1c3f). Przycisk pełnego ekranu dalej
|
||||
// pozwala wejść w poziom ręcznie, więc wyłączenie nie odbiera funkcji.
|
||||
const { autoRotate } = usePreferences();
|
||||
React.useEffect(() => {
|
||||
ScreenOrientation.unlockAsync().catch(() => {});
|
||||
if (autoRotate) ScreenOrientation.unlockAsync().catch(() => {});
|
||||
else {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
||||
}
|
||||
return () => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
}, [autoRotate]);
|
||||
|
||||
if (mode === 'webview') return <EmbedWebViewPlayer params={params} />;
|
||||
return <NativeVideoPlayer params={params} />;
|
||||
|
|
|
|||
|
|
@ -104,6 +104,23 @@ export async function setDefaultQuality(v: DefaultQuality): Promise<void> {
|
|||
await SecureStore.setItemAsync(DEFAULT_QUALITY_KEY, String(v));
|
||||
}
|
||||
|
||||
// Auto-obrót w playerze. Player domyślnie odblokowuje orientację (unlockAsync), żeby
|
||||
// dało się oglądać poziomo — ale wtedy telefon położony płasko na stole sam skacze
|
||||
// między orientacjami (bug-report 55da1c3f: "I can't turn off auto rotate. Which is a
|
||||
// problem when I put the phone on a flat surface"). Off = player zostaje w pionie,
|
||||
// a przełącznik pełnego ekranu dalej pozwala wejść w poziom RĘCZNIE.
|
||||
// Default true = dotychczasowe zachowanie.
|
||||
const AUTO_ROTATE_KEY = 'goon.player_auto_rotate';
|
||||
|
||||
export async function getAutoRotate(): Promise<boolean> {
|
||||
const v = await SecureStore.getItemAsync(AUTO_ROTATE_KEY);
|
||||
return v === null ? true : v === '1';
|
||||
}
|
||||
|
||||
export async function setAutoRotate(on: boolean): Promise<void> {
|
||||
await SecureStore.setItemAsync(AUTO_ROTATE_KEY, on ? '1' : '0');
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue