This commit is contained in:
team3
2026-09-18 23:47:25 +02:00
parent db77c1f985
commit a90044b0a7
21 changed files with 1111 additions and 253 deletions

View File

@@ -4,6 +4,7 @@ import MovieCard from './components/MovieCard.vue'
import FilterSidebar from './components/FilterSidebar.vue'
import AehnlichTab from './components/AehnlichTab.vue'
import KiTab from './components/KiTab.vue'
import PersonenTab from './components/PersonenTab.vue'
import MerklisteTab from './components/MerklisteTab.vue'
import SammlungFilter from './components/SammlungFilter.vue'
import MovieDetail from './components/MovieDetail.vue'
@@ -24,11 +25,20 @@ import { useLangeListe } from './composables/useLangeListe'
const { ansicht, ohneGesehene, activeTab, quelle, suchArt, host } = useEinstellungen()
const {
runde, ich, michSetzen,
gesehen, gesehenIds, gesehenZahl, ladeGesehen,
meinVorschlag, aufDemRad, ladeVorschlag,
merkliste, ladeMerkliste,
wuensche, ladeWuensche,
runde,
ich,
michSetzen,
gesehen,
gesehenIds,
gesehenZahl,
ladeGesehen,
meinVorschlag,
aufDemRad,
ladeVorschlag,
merkliste,
ladeMerkliste,
wuensche,
ladeWuensche,
} = useMeins({ host })
// Ohne gewählten Namen darf man alles lesen, aber nichts eintragen —
@@ -37,44 +47,74 @@ const {
const angemeldet = computed(() => !!ich.value)
const {
movies, loading, error, load,
netflixMovies, canonMovies, canonGroups, netflixEntries, SORTS,
movies,
loading,
error,
load,
netflixMovies,
canonMovies,
canonGroups,
netflixEntries,
SORTS,
} = useKatalog()
const {
search, sort, suchSort, include, exclude, grenzen, ranges, genreOptions,
toggleGenre, toggleAll, resetGenres, nichtGesehen, passt, suchFilter,
} = useFilter({ activeTab, quelle, netflixMovies, canonMovies, gesehenIds, ohneGesehene })
search,
sort,
suchSort,
suchAnzahl,
include,
exclude,
grenzen,
ranges,
genreOptions,
toggleGenre,
toggleAll,
resetGenres,
nichtGesehen,
passt,
suchFilter,
} = useFilter({
activeTab,
quelle,
suchArt,
netflixMovies,
canonMovies,
gesehenIds,
ohneGesehene,
})
// Eine Reihe erscheint, sobald ein Teil passt — sortiert wird sie über
// ihren stärksten Teil
function filterGruppen(entries, term) {
return entries
.filter(
(e) =>
e.parts.some((m) => passt(m)) &&
(!term || e.parts.some((m) => matches(m, term)))
(e) => e.parts.some((m) => passt(m)) && (!term || e.parts.some((m) => matches(m, term))),
)
.sort((a, b) => SORTS[sort.value](a.parts[a.best], b.parts[b.best]))
}
const visible = computed(() =>
filterGruppen(netflixEntries.value, search.value.trim().toLowerCase())
filterGruppen(netflixEntries.value, search.value.trim().toLowerCase()),
)
// Im Kanon greift die Suche auf die ganze Reihe: wer "Saw" sucht, will den
// Eintrag sehen, egal welcher Teil gerade vorne liegt.
const visibleCanon = computed(() =>
filterGruppen(canonGroups.value, search.value.trim().toLowerCase())
filterGruppen(canonGroups.value, search.value.trim().toLowerCase()),
)
const aktuelleListe = computed(() =>
quelle.value === 'netflix' ? visible.value : visibleCanon.value
quelle.value === 'netflix' ? visible.value : visibleCanon.value,
)
// Die zwei Listen des Katalogs, umschaltbar in der Filterleiste
const quellen = computed(() => [
{ id: 'netflix', label: 'Netflix', count: netflixMovies.value.filter(nichtGesehen).length },
{
id: 'netflix',
label: 'Netflix',
count: netflixMovies.value.filter(nichtGesehen).length,
},
{
id: 'canon',
label: 'Horror-Kanon',
@@ -98,7 +138,11 @@ const tabs = computed(() => [
{ id: 'wheel', label: 'Rad', count: null },
{ id: 'stream', label: 'Stream', count: null },
{ id: 'watched', label: 'Gesehen', count: gesehenZahl.value },
{ id: 'wuensche', label: 'Wünsche', count: wuensche.value.filter((w) => !w.done_at).length },
{
id: 'wuensche',
label: 'Wünsche',
count: wuensche.value.filter((w) => !w.done_at).length,
},
...(host.value || angemeldet.value
? [{ id: 'verwaltung', label: 'Verwaltung', count: null }]
: []),
@@ -107,9 +151,7 @@ const tabs = computed(() => [
// Auf dem Handy passt die Filterleiste nicht neben die Karten — dort
// legt sie sich über einen Knopf über die Seite
const filterOffen = ref(false)
const mitFilter = computed(() =>
['katalog', 'suche', 'merkliste'].includes(activeTab.value)
)
const mitFilter = computed(() => ['katalog', 'suche', 'merkliste'].includes(activeTab.value))
// Der Sammlungen-Reiter filtert nach Person und Genre, nicht nach Regler —
// dafür hat er eine eigene Leiste
@@ -124,9 +166,7 @@ const leuteMitSammlung = computed(() => {
if (!nach.has(name)) nach.set(name, { name, anzahl: 0, mine: eintrag.mine })
nach.get(name).anzahl += 1
}
return [...nach.values()].sort(
(a, b) => b.mine - a.mine || a.name.localeCompare(b.name, 'de')
)
return [...nach.values()].sort((a, b) => b.mine - a.mine || a.name.localeCompare(b.name, 'de'))
})
// Fällt der offene Reiter weg — Hostmodus aus, Name abgewählt —, wäre
@@ -180,9 +220,7 @@ const series = computed(() => {
: []
if (group.length < 2) return []
return [...group].sort((a, b) =>
(a.release_date || '').localeCompare(b.release_date || '')
)
return [...group].sort((a, b) => (a.release_date || '').localeCompare(b.release_date || ''))
})
// Aus der Filmansicht heraus zur Ähnlichkeitsuche springen
@@ -195,10 +233,21 @@ function zurAehnlichkeitsuche(movie) {
suchArt.value = 'aehnlich'
}
// Aus der Filmansicht zu einer Person: Regie oder Besetzung
const personStart = ref(null)
function zurPersonensuche(person) {
personStart.value = person
selected.value = null
activeTab.value = 'suche'
suchArt.value = 'personen'
}
// Die drei Suchen unter einem Reiter, umschaltbar über der Liste
const SUCHARTEN = [
{ id: 'search', label: 'Titel' },
{ id: 'aehnlich', label: 'Ähnliche Filme' },
{ id: 'personen', label: 'Personen' },
{ id: 'ki', label: 'KI' },
]
@@ -238,12 +287,7 @@ onMounted(load)
Filter
</button>
<span v-if="host" class="host">Host</span>
<NamensWahl
:items="runde"
:mine="ich"
:host="host"
:anmelden="michSetzen"
/>
<NamensWahl :items="runde" :mine="ich" :host="host" :anmelden="michSetzen" />
</div>
</header>
@@ -280,6 +324,17 @@ onMounted(load)
@gestartet="aehnlichStart = null"
/>
<PersonenTab
v-else-if="suchArt === 'personen'"
:gesehen-ids="gesehenIds"
:ohne-gesehene="ohneGesehene"
:filter="suchFilter"
:ansicht="ansicht"
:start="personStart"
@open="selected = $event"
@gestartet="personStart = null"
/>
<KiTab
v-else
:angemeldet="angemeldet"
@@ -294,6 +349,8 @@ onMounted(load)
<!-- Die KI ordnet selbst nach Passung, da gibt es nichts zu sortieren -->
<FilterSidebar
v-model:sort="suchSort"
v-model:anzahl="suchAnzahl"
:mit-anzahl="suchArt !== 'personen'"
v-model:ansicht="ansicht"
v-model:ohne-gesehene="ohneGesehene"
v-model:ranges="ranges"
@@ -363,12 +420,7 @@ onMounted(load)
@changed="ladeWuensche"
/>
<VerwaltungTab
v-else-if="activeTab === 'verwaltung'"
:items="runde"
:host="host"
:ich="ich"
/>
<VerwaltungTab v-else-if="activeTab === 'verwaltung'" :items="runde" :host="host" :ich="ich" />
<StreamTab v-else-if="activeTab === 'stream'" />
@@ -459,6 +511,7 @@ onMounted(load)
@suggested="nachVorschlag"
@watched="ladeGesehen"
@aehnlich="zurAehnlichkeitsuche"
@person="zurPersonensuche"
/>
</div>
</template>
@@ -649,10 +702,7 @@ onMounted(load)
/* Eine Detailkarte ist so breit wie vier kompakte Karten nebeneinander —
oder die halbe Reihe, falls die schmaler ist. */
.grid.details {
grid-template-columns: repeat(
auto-fill,
min(calc(50% - 0.55rem), calc(4 * 190px + 3 * 1.1rem))
);
grid-template-columns: repeat(auto-fill, min(calc(50% - 0.55rem), calc(4 * 190px + 3 * 1.1rem)));
}
.note {