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,