diff --git a/src/App.tsx b/src/App.tsx index c9489f7..629a5f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { createMemo, ErrorBoundary, Accessor } from "solid-js"; import { useKeyboard, useSelectionHandler } from "@opentui/solid"; import { TabNavigation } from "./components/TabNavigation"; import { CodeValidation } from "@/components/CodeValidation"; +import { LoadingIndicator } from "@/components/LoadingIndicator"; import { useAuthStore } from "@/stores/auth"; import { useFeedStore } from "@/stores/feed"; import { useAudio } from "@/hooks/useAudio"; @@ -89,10 +90,13 @@ export function App() { audioSeekBackward: isSeekBackward, quit: isQuit, }); - if (isCycle) { - } - // only handling top + // only handling top navigation here, cycle through tabs, just to high priority(player) all else to be handled in each tab + if (nav.activeDepth == 0) { + if (isCycle) { + nav.nextTab(); + } + } }, { release: false }, ); @@ -109,6 +113,7 @@ export function App() { )} > + {DEBUG && ( @@ -149,7 +154,6 @@ export function App() { onTabSelect={nav.setActiveTab} /> {LayerGraph[nav.activeTab]()} - {/** TODO: Contextual controls based on tab/depth**/} ); diff --git a/src/components/LoadingIndicator.tsx b/src/components/LoadingIndicator.tsx new file mode 100644 index 0000000..85a0a8d --- /dev/null +++ b/src/components/LoadingIndicator.tsx @@ -0,0 +1,36 @@ +/** + * Loading indicator component + * Displays an animated sliding bar at the top of the screen + */ + +import { For } from "solid-js"; +import { useTheme } from "@/context/ThemeContext"; + +interface LoadingIndicatorProps { + isLoading: boolean; +} + +export function LoadingIndicator(props: LoadingIndicatorProps) { + const { theme } = useTheme(); + + if (!props.isLoading) return null; + + return ( + + + {(_, index) => ( + + )} + + + ); +} diff --git a/src/config/keybind.jsonc b/src/config/keybind.jsonc index 0dff2b2..6e85465 100644 --- a/src/config/keybind.jsonc +++ b/src/config/keybind.jsonc @@ -9,6 +9,7 @@ "inverse": ["shift"], "leader": ":", // will not trigger while focused on input "quit": ["q"], + "refresh": ["r"], "audio-toggle": ["p"], "audio-pause": [], "audio-play": [], diff --git a/src/context/KeybindContext.tsx b/src/context/KeybindContext.tsx index d33be69..248645f 100644 --- a/src/context/KeybindContext.tsx +++ b/src/context/KeybindContext.tsx @@ -60,6 +60,7 @@ export const { use: useKeybinds, provider: KeybindProvider } = inverse: [], leader: "", quit: [], + refresh: [], "audio-toggle": [], "audio-pause": [], "audio-play": [], @@ -95,9 +96,6 @@ export const { use: useKeybinds, provider: KeybindProvider } = for (const key of keys) { if (evt.name === key) return true; - if (evt.shift && key.toLowerCase() !== key) return false; - if (evt.ctrl && !key.toLowerCase().includes("ctrl")) return false; - if (evt.meta && !key.toLowerCase().includes("meta")) return false; } return false; } diff --git a/src/context/NavigationContext.tsx b/src/context/NavigationContext.tsx index ec96fe7..296d204 100644 --- a/src/context/NavigationContext.tsx +++ b/src/context/NavigationContext.tsx @@ -1,27 +1,48 @@ import { createSignal } from "solid-js"; import { createSimpleContext } from "./helper"; -import { TABS } from "../utils/navigation"; +import { TABS, TabsCount } from "@/utils/navigation"; -export const { use: useNavigation, provider: NavigationProvider } = createSimpleContext({ - name: "Navigation", - init: () => { - const [activeTab, setActiveTab] = createSignal(TABS.FEED); - const [activeDepth, setActiveDepth] = createSignal(0); - const [inputFocused, setInputFocused] = createSignal(false); +export const { use: useNavigation, provider: NavigationProvider } = + createSimpleContext({ + name: "Navigation", + init: () => { + const [activeTab, setActiveTab] = createSignal(TABS.FEED); + const [activeDepth, setActiveDepth] = createSignal(0); + const [inputFocused, setInputFocused] = createSignal(false); - return { - get activeTab() { - return activeTab(); - }, - get activeDepth() { - return activeDepth(); - }, - get inputFocused() { - return inputFocused(); - }, - setActiveTab, - setActiveDepth, - setInputFocused, - }; - }, -}); + //conveniences + const nextTab = () => { + if (activeTab() >= TabsCount) { + setActiveTab(1); + return; + } + setActiveTab(activeTab() + 1); + }; + + const prevTab = () => { + if (activeTab() <= 1) { + setActiveTab(TabsCount); + return; + } + + setActiveTab(activeTab() - 1); + }; + + return { + get activeTab() { + return activeTab(); + }, + get activeDepth() { + return activeDepth(); + }, + get inputFocused() { + return inputFocused(); + }, + setActiveTab, + setActiveDepth, + setInputFocused, + nextTab, + prevTab, + }; + }, + }); diff --git a/src/stores/feed.ts b/src/stores/feed.ts index ea4aca9..b7cfbe8 100644 --- a/src/stores/feed.ts +++ b/src/stores/feed.ts @@ -48,13 +48,6 @@ export function createFeedStore() { const [sources, setSources] = createSignal([ ...DEFAULT_SOURCES, ]); - - (async () => { - const loadedFeeds = await loadFeedsFromFile(); - if (loadedFeeds.length > 0) setFeeds(loadedFeeds); - const loadedSources = await loadSourcesFromFile(); - if (loadedSources && loadedSources.length > 0) setSources(loadedSources); - })(); const [filter, setFilter] = createSignal({ visibility: "all", sortBy: "updated" as FeedSortField, @@ -62,6 +55,7 @@ export function createFeedStore() { }); const [selectedFeedId, setSelectedFeedId] = createSignal(null); const [isLoadingMore, setIsLoadingMore] = createSignal(false); + const [isLoadingFeeds, setIsLoadingFeeds] = createSignal(false); /** Get filtered and sorted feeds */ const getFilteredFeeds = (): Feed[] => { @@ -148,6 +142,13 @@ export function createFeedStore() { return allEpisodes; }; + /** Sort episodes in reverse chronological order (newest first) */ + const sortEpisodesReverseChronological = (episodes: Episode[]): Episode[] => { + return [...episodes].sort( + (a, b) => b.pubDate.getTime() - a.pubDate.getTime(), + ); + }; + /** Fetch latest episodes from an RSS feed URL, caching all parsed episodes */ const fetchEpisodes = async ( feedUrl: string, @@ -164,7 +165,7 @@ export function createFeedStore() { if (!response.ok) return []; const xml = await response.text(); const parsed = parseRSSFeed(xml, feedUrl); - const allEpisodes = parsed.episodes; + const allEpisodes = sortEpisodesReverseChronological(parsed.episodes); // Cache all parsed episodes for pagination if (feedId) { @@ -264,12 +265,25 @@ export function createFeedStore() { /** Refresh all feeds */ const refreshAllFeeds = async () => { - const currentFeeds = feeds(); - for (const feed of currentFeeds) { - await refreshFeed(feed.id); + setIsLoadingFeeds(true); + try { + const currentFeeds = feeds(); + for (const feed of currentFeeds) { + await refreshFeed(feed.id); + } + } finally { + setIsLoadingFeeds(false); } }; + (async () => { + const loadedFeeds = await loadFeedsFromFile(); + if (loadedFeeds.length > 0) setFeeds(loadedFeeds); + const loadedSources = await loadSourcesFromFile(); + if (loadedSources && loadedSources.length > 0) setSources(loadedSources); + await refreshAllFeeds(); + })(); + /** Remove a feed */ const removeFeed = (feedId: string) => { fullEpisodeCache.delete(feedId); @@ -445,6 +459,7 @@ export function createFeedStore() { getFeed, getSelectedFeed, hasMoreEpisodes, + isLoadingFeeds, // Actions setFilter,