fix(nav): a linked player, file and search survive what the app does by itself (review round 2)
- Playlists: a link over a playlist the account does not hold plays alone; the rail's own fallback no longer counts as a pick that gives the player up. - The linked opener opens or clears nothing once the address names another player, and stops looking (and fetching its row) once it settled. - Files: a list the server's cap cut — the sorted or searched one too — keeps the link, with a notice of its own (EN/HU). - History: an in-place view rewrite (search refine, count, channel to channel, sub-view) drops a reloaded entry's overlay marks (replaceView); a landing on a channel opened from a search keeps the search; the Feed press on a search opens the plain feed. - Files/Playlists: an emptied search is written at once. - Client log: plf/cf (column text filters) are typed keys; the shell has none of its own. - Contract: writers outside lib/ take a literal or a lib/ function's result; every URL key is sorted typed or not; the writer regexes are held by a fixture; sub-views named; the bookmark net reads page tags. - Feed defaults in one constant; one live-search queue helper; Scheduler writes through the Activity codec; notRun copy (EN/HU) names Forward and a plain "Search YouTube" button.
This commit is contained in:
@@ -269,7 +269,7 @@ export default function Feed({
|
||||
const { setYtCount, askYtSearch } = useNavigationActions();
|
||||
// A search runs by itself only where nobody's reload or link can spend quota with it: the free
|
||||
// page scrape, or a search asked for in this document. An API search reopened by a reload, a link
|
||||
// or Back waits for "Search YouTube again" — unless its results are still cached, which a disabled
|
||||
// or Back waits for "Search YouTube" — unless its results are still cached, which a disabled
|
||||
// query shows all the same. Turned off on this instance it runs too: the server refuses it before
|
||||
// any quota, and its refusal is what says so (no press offered that could only be refused).
|
||||
const ytRuns = ytActive && (me.search_source !== "api" || ytSearchAsked || !searchOn);
|
||||
@@ -506,7 +506,7 @@ export default function Feed({
|
||||
// The live search's cards, in the order picked for them, and the player's queue over them.
|
||||
const ytShown = (rows: Video[]): Video[] =>
|
||||
sortYtResults(withOverrides(rows, true), ytSort.key as SortKey, ytSort.dir, lang);
|
||||
const ytQueue = (rows: Video[]): Video[] => ytShown(rows).filter((v) => v.status !== "hidden");
|
||||
const ytQueueOf = (shown: Video[]): Video[] => shown.filter((v) => v.status !== "hidden");
|
||||
const merged: Video[] = withOverrides(loaded);
|
||||
const items: Video[] = merged.filter((v) => matchesView(v.status, filters.show));
|
||||
// The in-app player's queue (auto-advance / loop / prev-next). It's the filtered feed, but the
|
||||
@@ -520,7 +520,7 @@ export default function Feed({
|
||||
// list it was linked over: leaving the search first gives it up.
|
||||
useOpenLinkedPlayer(
|
||||
ytActive ? ytQuery.data !== undefined || ytQuery.isError || !ytRuns : !query.isLoading,
|
||||
(id) => (ytActive ? ytQueue(ytLoaded) : playerQueue).find((v) => v.id === id),
|
||||
(id) => (ytActive ? ytQueueOf(ytShown(ytLoaded)) : playerQueue).find((v) => v.id === id),
|
||||
(video, inList) => {
|
||||
openVideo(video);
|
||||
// Not in what this list has loaded (further down it, or no longer in it): it plays alone.
|
||||
@@ -541,7 +541,7 @@ export default function Feed({
|
||||
// user explicitly searched, so show everything) and no auto-pagination (each page costs quota).
|
||||
if (ytActive) {
|
||||
const ytItems: Video[] = ytShown(ytLoaded);
|
||||
const ytPlayerQueue: Video[] = ytItems.filter((v) => v.status !== "hidden");
|
||||
const ytPlayerQueue: Video[] = ytQueueOf(ytItems);
|
||||
const ytError =
|
||||
ytQuery.error instanceof HttpError
|
||||
? ytQuery.error.detail || t("feed.yt.error")
|
||||
@@ -649,7 +649,7 @@ export default function Feed({
|
||||
<div className="py-16 text-center text-muted">{t("feed.yt.searching")}</div>
|
||||
) : ytError && !ytItems.length ? (
|
||||
// A first page that failed is said as the error it is, with Retry where trying again can
|
||||
// help (YouTube busy or unreachable): Search YouTube again with the same term reads the
|
||||
// help (YouTube busy or unreachable): Search YouTube with the same term reads the
|
||||
// failure from the cache and sends nothing.
|
||||
<StateMessage
|
||||
tone="error"
|
||||
|
||||
@@ -147,7 +147,7 @@ describe("Feed — a live search's Load more that fails", () => {
|
||||
});
|
||||
|
||||
describe("Feed — a live search whose first page fails", () => {
|
||||
// Search YouTube again with the same term reads the cached failure: Retry is the way to ask again.
|
||||
// Search YouTube with the same term reads the cached failure: Retry is the way to ask again.
|
||||
it("offers Retry where trying again can help, and reads the page on it", async () => {
|
||||
const search = vi
|
||||
.spyOn(api, "searchYoutube")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// A live YouTube search the address reopens (a reload, a link, Back) runs by itself only where it
|
||||
// costs no quota: the free page scrape. An API search spends 100 units, so it waits for "Search
|
||||
// YouTube again" — unless its results are still cached, or it was asked for in this document.
|
||||
// YouTube" — unless its results are still cached, or it was asked for in this document.
|
||||
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { coerceTagModes, coerceUntagged, DEFAULT_TAG_MODES, type FeedFilters } from "../../lib/api";
|
||||
import {
|
||||
coerceTagModes,
|
||||
coerceUntagged,
|
||||
DEFAULT_TAG_MODES,
|
||||
FEED_DEFAULT_SHOW,
|
||||
FEED_DEFAULT_SORT,
|
||||
type FeedFilters,
|
||||
} from "../../lib/api";
|
||||
import { LS, ownKey, readJSON } from "../../lib/storage";
|
||||
|
||||
// The feed filters' defaults and the per-account localStorage seed, split out of FeedFiltersProvider
|
||||
@@ -10,12 +17,12 @@ export const DEFAULT_FILTERS: FeedFilters = {
|
||||
untagged: false,
|
||||
channelIds: [],
|
||||
q: "",
|
||||
sort: "newest",
|
||||
sort: FEED_DEFAULT_SORT,
|
||||
scope: "my",
|
||||
includeNormal: true,
|
||||
includeShorts: false,
|
||||
includeLive: false,
|
||||
show: "unwatched",
|
||||
show: FEED_DEFAULT_SHOW,
|
||||
};
|
||||
|
||||
/** The watch-state choices, in the order the filter offers them. */
|
||||
|
||||
Reference in New Issue
Block a user