mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f361be0c72 | ||
|
|
1611dc5703 | ||
|
|
04faad84a4 | ||
|
|
19c42e5838 | ||
|
|
4918b69d0a | ||
|
|
c7cec464aa | ||
|
|
91857c4d73 | ||
|
|
fc6f9f4258 | ||
|
|
34f9f9374a | ||
|
|
0ae4c1621f | ||
|
|
c393f5c045 | ||
|
|
1624290dc1 | ||
|
|
c6491990ac | ||
|
|
15dea17923 | ||
|
|
689d5ac7b2 | ||
|
|
2142e20e7d | ||
|
|
dc23126570 | ||
|
|
55856f9060 | ||
|
|
c756ce5fc8 | ||
|
|
0546f25d55 | ||
|
|
7b33717333 | ||
|
|
6ea6d16e58 | ||
|
|
a9b65c83aa | ||
|
|
a497802b50 | ||
|
|
42b0428b9a | ||
|
|
931c553e1d | ||
|
|
f3c0b92a3c | ||
|
|
970cabf241 | ||
|
|
e321ecde5d | ||
|
|
32ac326a77 | ||
|
|
134dcd4466 | ||
|
|
26a44353d4 | ||
|
|
55acb3ef28 | ||
|
|
0e96307726 | ||
|
|
0199a36238 | ||
|
|
3f2f6e83fa | ||
|
|
4fa780cac2 | ||
|
|
edb0f655b0 | ||
|
|
651ada7073 | ||
|
|
efb5d49d04 | ||
|
|
f78cc18b06 | ||
|
|
8acffa11e5 | ||
|
|
f4246807ff | ||
|
|
abf6e7131b | ||
|
|
b2688520cc | ||
|
|
fad0aea108 | ||
|
|
0b63773c83 | ||
|
|
3ef28009ac | ||
|
|
8979e2b191 | ||
|
|
d6910aa1e8 | ||
|
|
afc56c6053 | ||
|
|
1bd504cbfb | ||
|
|
2c089ddb5e | ||
|
|
0b5245643a | ||
|
|
ae35d43f7f | ||
|
|
fe55682c9f | ||
|
|
0d3e6f17e2 | ||
|
|
d5659c4278 | ||
|
|
69b87b9026 | ||
|
|
168bcd3a37 | ||
|
|
e3b6be0cd0 | ||
|
|
eeceda0ca8 | ||
|
|
aa903039c8 | ||
|
|
73d81d0cdb | ||
|
|
01fe539af6 | ||
|
|
c08063ca57 | ||
|
|
60d4af2890 | ||
|
|
6378f074a8 | ||
|
|
5082ec86fd | ||
|
|
6cff5bb099 | ||
|
|
d54562d56f | ||
|
|
2b45a8fae5 | ||
|
|
8654df8994 | ||
|
|
4d5145c17e | ||
|
|
b5c197f499 | ||
|
|
d417655a86 |
41
.github/workflows/ci.yml
vendored
41
.github/workflows/ci.yml
vendored
@@ -44,18 +44,27 @@ jobs:
|
||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
|
||||
|
||||
# Build pages
|
||||
- name: Copy generated markdown documentation to /documentation
|
||||
run: mkdir documentation && cp ./commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md ./documentation/README.md
|
||||
- name: Create pages directory structure
|
||||
run: mkdir -p target/pages/documentation/custom-css
|
||||
|
||||
- name: Generate pages
|
||||
uses: wranders/markdown-to-pages-action@8d8a750832932ac785f5424c8c5543aa0b26bb9a # v1
|
||||
- name: Convert readme file to html
|
||||
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
out_path: target/pages
|
||||
files: |-
|
||||
README.md
|
||||
documentation/README.md
|
||||
|
||||
source: README.md
|
||||
output: target/pages/index.html
|
||||
|
||||
- name: Convert config documentation to html
|
||||
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
|
||||
with:
|
||||
source: commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md
|
||||
output: target/pages/documentation/index.html
|
||||
|
||||
- name: Convert custom css documentation to html
|
||||
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
|
||||
with:
|
||||
source: documentation/CUSTOMCSS.md
|
||||
output: target/pages/documentation/custom-css/index.html
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload cross-platform app
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
@@ -134,7 +143,7 @@ jobs:
|
||||
|
||||
## build but don't push for PRs and renovate
|
||||
- name: Docker build - native
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
@@ -142,7 +151,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
- name: Docker build - jvm
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
@@ -151,7 +160,7 @@ jobs:
|
||||
|
||||
## build and push tag
|
||||
- name: Docker build and push tag - native
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -163,7 +172,7 @@ jobs:
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push tag - jvm
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -176,7 +185,7 @@ jobs:
|
||||
|
||||
## build and push master
|
||||
- name: Docker build and push master - native
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -186,7 +195,7 @@ jobs:
|
||||
tags: athou/commafeed:master-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push master - jvm
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [5.10.0]
|
||||
|
||||
- Add an indicator next to each feed's unread count in the tree to show when new entries are discovered while the app is open (#1762)
|
||||
- Feeds with uppercase HTTP:// or HTTPS:// URLs are now correctly handled again
|
||||
- The aarch64 native executable now also works on the Raspberry Pi 5 (#1795)
|
||||
- Improve general performance of the UI by reducing the number of re-renders, especially when a lot of entries are displayed (#1087)
|
||||
|
||||
## [5.9.0]
|
||||
|
||||
- A lot of CSS classes have been added to the elements of the application to ease custom CSS rules (#1757)
|
||||
- Added a link in the README to the [documentation](https://athou.github.io/commafeed/documentation/custom-css/) of the new CSS classes
|
||||
- Static resources are now cached for much longer (#1782)
|
||||
|
||||
## [5.8.0]
|
||||
|
||||
- A color picker is now available on the settings page to change the orange accent of the application (#1598)
|
||||
|
||||
@@ -17,6 +17,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc
|
||||
- REST API
|
||||
- Fever-compatible API for native mobile apps
|
||||
- Can automatically mark articles as read based on user-defined rules
|
||||
- Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript
|
||||
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
|
||||
- Compiles to native code for blazing fast startup and low memory usage
|
||||
- Supports 4 databases
|
||||
|
||||
887
commafeed-client/package-lock.json
generated
887
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,14 +19,14 @@
|
||||
"@fontsource/open-sans": "^5.2.5",
|
||||
"@lingui/core": "^5.3.1",
|
||||
"@lingui/react": "^5.3.1",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/form": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@mantine/modals": "^8.0.0",
|
||||
"@mantine/notifications": "^8.0.0",
|
||||
"@mantine/spotlight": "^8.0.0",
|
||||
"@mantine/core": "^8.0.2",
|
||||
"@mantine/form": "^8.0.2",
|
||||
"@mantine/hooks": "^8.0.2",
|
||||
"@mantine/modals": "^8.0.2",
|
||||
"@mantine/notifications": "^8.0.2",
|
||||
"@mantine/spotlight": "^8.0.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@reduxjs/toolkit": "^2.7.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"axios": "^1.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
@@ -43,13 +43,13 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.5.3",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"redoc": "^2.5.0",
|
||||
"style-to-object": "^1.0.8",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tinycon": "^0.6.8",
|
||||
"tss-react": "^4.9.17",
|
||||
"tss-react": "^4.9.18",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -61,20 +61,21 @@
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^19.1.3",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/react-infinite-scroller": "^1.2.5",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycon": "^0.6.7",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"rollup-plugin-visualizer": "^6.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-checker": "^0.9.2",
|
||||
"vite-plugin-checker": "^0.9.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.3"
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"overrides": {
|
||||
"react-infinite-scroller": {
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>5.8.0</version>
|
||||
<version>5.10.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
<properties>
|
||||
<!-- renovate: datasource=node-version depName=node -->
|
||||
<node.version>v22.15.0</node.version>
|
||||
<node.version>v22.16.0</node.version>
|
||||
<!-- renovate: datasource=npm depName=npm -->
|
||||
<npm.version>11.3.0</npm.version>
|
||||
<npm.version>11.4.1</npm.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -143,11 +143,15 @@ function GoogleAnalyticsHandler() {
|
||||
return null
|
||||
}
|
||||
|
||||
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
function UnreadCountTitleHandler({ enabled }: { enabled?: boolean }) {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title>
|
||||
}
|
||||
|
||||
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
function UnreadCountFaviconHandler({ enabled }: { enabled?: boolean }) {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
useEffect(() => {
|
||||
if (enabled && unreadCount > 0) {
|
||||
Tinycon.setBubble(unreadCount)
|
||||
@@ -205,13 +209,10 @@ function CustomCssHandler() {
|
||||
|
||||
export function App() {
|
||||
useI18n()
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(reloadServerInfos())
|
||||
}, [dispatch])
|
||||
@@ -219,8 +220,8 @@ export function App() {
|
||||
return (
|
||||
<Providers>
|
||||
<>
|
||||
<UnreadCountTitleHandler unreadCount={unreadCount} enabled={unreadCountTitle} />
|
||||
<UnreadCountFaviconHandler unreadCount={unreadCount} enabled={unreadCountFavicon} />
|
||||
<UnreadCountTitleHandler enabled={unreadCountTitle} />
|
||||
<UnreadCountFaviconHandler enabled={unreadCountFavicon} />
|
||||
<BrowserExtensionBadgeUnreadCountHandler />
|
||||
<CustomJsHandler />
|
||||
<CustomCssHandler />
|
||||
|
||||
@@ -108,5 +108,6 @@ export const Constants = {
|
||||
delay: 500,
|
||||
},
|
||||
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
||||
customCssDocumentationUrl: "https://athou.github.io/commafeed/documentation/custom-css",
|
||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { markEntry } from "app/entries/thunks"
|
||||
import { loadEntries, markEntry } from "app/entries/thunks"
|
||||
import { redirectTo } from "app/redirect/slice"
|
||||
import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
|
||||
import type { Category } from "app/types"
|
||||
import { visitCategoryTree } from "app/utils"
|
||||
import type { Category, Subscription } from "app/types"
|
||||
import { flattenCategoryTree, visitCategoryTree } from "app/utils"
|
||||
|
||||
export interface TreeSubscription extends Subscription {
|
||||
// client-side only flag
|
||||
hasNewEntries?: boolean
|
||||
}
|
||||
|
||||
export interface TreeCategory extends Category {
|
||||
feeds: TreeSubscription[]
|
||||
children: TreeCategory[]
|
||||
}
|
||||
|
||||
interface TreeState {
|
||||
rootCategory?: Category
|
||||
rootCategory?: TreeCategory
|
||||
mobileMenuOpen: boolean
|
||||
sidebarVisible: boolean
|
||||
}
|
||||
@@ -37,12 +47,27 @@ export const treeSlice = createSlice({
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) {
|
||||
f.unread += action.payload.amount
|
||||
f.hasNewEntries = true
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadTree.fulfilled, (state, action) => {
|
||||
// set hasNewEntries to true if new unread > previous unread
|
||||
if (state.rootCategory) {
|
||||
const oldFeeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
|
||||
const oldFeedsById = new Map(oldFeeds.map(f => [f.id, f]))
|
||||
|
||||
const newFeeds = flattenCategoryTree(action.payload).flatMap(c => c.feeds)
|
||||
for (const newFeed of newFeeds) {
|
||||
const oldFeed = oldFeedsById.get(newFeed.id)
|
||||
if (oldFeed && newFeed.unread > oldFeed.unread) {
|
||||
newFeed.hasNewEntries = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.rootCategory = action.payload
|
||||
})
|
||||
builder.addCase(collapseTreeCategory.pending, (state, action) => {
|
||||
@@ -59,6 +84,25 @@ export const treeSlice = createSlice({
|
||||
}
|
||||
})
|
||||
})
|
||||
builder.addCase(loadEntries.fulfilled, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
|
||||
const { source } = action.meta.arg
|
||||
if (source.type === "category") {
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
if (c.id === source.id) {
|
||||
for (const f of flattenCategoryTree(c).flatMap(c => c.feeds)) {
|
||||
f.hasNewEntries = false
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (source.type === "feed") {
|
||||
const feeds = flattenCategoryTree(state.rootCategory).flatMap(c => c.feeds)
|
||||
for (const f of feeds.filter(f => f.id === +source.id)) {
|
||||
f.hasNewEntries = false
|
||||
}
|
||||
}
|
||||
})
|
||||
builder.addCase(redirectTo, state => {
|
||||
state.mobileMenuOpen = false
|
||||
})
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { loadEntries } from "app/entries/thunks"
|
||||
import { type RootState, reducers } from "app/store"
|
||||
import { selectNextUnreadTreeItem } from "app/tree/thunks"
|
||||
import type { Category, Subscription } from "app/types"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { newFeedEntriesDiscovered, selectNextUnreadTreeItem } from "app/tree/thunks"
|
||||
import type { Category, Entries, Entry, Subscription } from "app/types"
|
||||
import type { AxiosResponse } from "axios"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
vi.mock(import("app/client"))
|
||||
|
||||
const createCategory = (id: string): Category => ({
|
||||
id,
|
||||
@@ -117,3 +122,51 @@ describe("selectNextUnreadTreeItem", () => {
|
||||
expect(store.getState().redirect.to).toBe("/app/feed/3")
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasNewEntries", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("sets and clear flag for a feed", async () => {
|
||||
vi.mocked(client.feed.getEntries).mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: false,
|
||||
name: "my-feed",
|
||||
errorCount: 3,
|
||||
feedLink: "https://mysite.com/feed",
|
||||
timestamp: 123,
|
||||
ignoredReadStatus: false,
|
||||
},
|
||||
} as AxiosResponse<Entries>)
|
||||
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
tree: {
|
||||
rootCategory: root,
|
||||
},
|
||||
entries: {
|
||||
source: {
|
||||
type: "feed",
|
||||
id: "1",
|
||||
},
|
||||
},
|
||||
} as RootState,
|
||||
})
|
||||
|
||||
// initial state
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].unread).toBe(0)
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBeFalsy()
|
||||
|
||||
// increments unread count and sets hasNewEntries to true
|
||||
await store.dispatch(newFeedEntriesDiscovered({ feedId: 1, amount: 3 }))
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].unread).toBe(3)
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBe(true)
|
||||
|
||||
// reload entries and sets hasNewEntries to false
|
||||
await store.dispatch(loadEntries({ source: { type: "feed", id: "1" }, clearSearch: true }))
|
||||
expect(store.getState().tree.rootCategory?.children[0].feeds[0].hasNewEntries).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { TreeCategory } from "app/tree/slice"
|
||||
import { throttle } from "throttle-debounce"
|
||||
import type { Category } from "./types"
|
||||
|
||||
export function visitCategoryTree(
|
||||
category: Category,
|
||||
visitor: (category: Category) => void,
|
||||
category: TreeCategory,
|
||||
visitor: (category: TreeCategory) => void,
|
||||
options?: {
|
||||
childrenFirst?: boolean
|
||||
}
|
||||
@@ -19,13 +20,13 @@ export function visitCategoryTree(
|
||||
if (childrenFirst) visitor(category)
|
||||
}
|
||||
|
||||
export function flattenCategoryTree(category: Category): Category[] {
|
||||
export function flattenCategoryTree(category: TreeCategory): TreeCategory[] {
|
||||
const categories: Category[] = []
|
||||
visitCategoryTree(category, c => categories.push(c))
|
||||
return categories
|
||||
}
|
||||
|
||||
export function categoryUnreadCount(category?: Category): number {
|
||||
export function categoryUnreadCount(category?: TreeCategory): number {
|
||||
if (!category) return 0
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
@@ -34,6 +35,14 @@ export function categoryUnreadCount(category?: Category): number {
|
||||
.reduce((total, current) => total + current, 0)
|
||||
}
|
||||
|
||||
export function categoryHasNewEntries(category?: TreeCategory): boolean {
|
||||
if (!category) return false
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
.flatMap(c => c.feeds)
|
||||
.some(f => f.hasNewEntries)
|
||||
}
|
||||
|
||||
export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => {
|
||||
const placeholderWidth = width && Math.min(width, maxWidth)
|
||||
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ActionButton = forwardRef<HTMLDivElement, ActionButtonProps>((props
|
||||
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<Box ref={ref} className="cf-action-button">
|
||||
{iconOnly && (
|
||||
<Tooltip label={label} openDelay={Constants.tooltip.delay}>
|
||||
<ActionIcon
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMobile } from "hooks/useMobile"
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
interface CodeEditorProps {
|
||||
label?: ReactNode
|
||||
description?: ReactNode
|
||||
language: "css" | "javascript"
|
||||
value?: string
|
||||
@@ -19,7 +20,8 @@ export function CodeEditor(props: CodeEditorProps) {
|
||||
autosize
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
label={props.description}
|
||||
label={props.label}
|
||||
description={props.description}
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily: "monospace",
|
||||
@@ -29,7 +31,7 @@ export function CodeEditor(props: CodeEditorProps) {
|
||||
onChange={e => props.onChange(e.currentTarget.value)}
|
||||
/>
|
||||
) : (
|
||||
<Input.Wrapper label={props.description}>
|
||||
<Input.Wrapper label={props.label} description={props.description}>
|
||||
<RichCodeEditor height="30vh" language={props.language} value={props.value} onChange={props.onChange} />
|
||||
</Input.Wrapper>
|
||||
)
|
||||
|
||||
@@ -288,7 +288,7 @@ export function FeedEntries() {
|
||||
return (
|
||||
<InfiniteScroll
|
||||
id="entries"
|
||||
className={`view-mode-${viewMode}`}
|
||||
className={`cf-entries cf-view-mode-${viewMode}`}
|
||||
initialLoad={false}
|
||||
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
|
||||
hasMore={hasMore}
|
||||
|
||||
@@ -184,10 +184,10 @@ export function FeedEntry(props: FeedEntryProps) {
|
||||
</a>
|
||||
{props.expanded && (
|
||||
<Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
|
||||
<Box className={classes.body}>
|
||||
<Box className={`${classes.body} cf-content`}>
|
||||
<FeedEntryBody entry={props.entry} />
|
||||
</Box>
|
||||
<Divider variant="dashed" my={paddingY} />
|
||||
<Divider variant="dashed" my={paddingY} className="cf-footer-divider" />
|
||||
<FeedEntryFooter entry={props.entry} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
)
|
||||
|
||||
return (
|
||||
<Group justify="space-between">
|
||||
<Group justify="space-between" className="cf-footer">
|
||||
<Group gap={spacing}>
|
||||
{props.entry.markable && (
|
||||
<ActionButton
|
||||
|
||||
@@ -29,8 +29,8 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
read: props.entry.read,
|
||||
})
|
||||
return (
|
||||
<Box>
|
||||
<Flex align="flex-start" justify="space-between">
|
||||
<Box className="cf-header">
|
||||
<Flex align="flex-start" justify="space-between" className="cf-header-title">
|
||||
<Flex align="flex-start" className={classes.main}>
|
||||
{props.showStarIcon && (
|
||||
<Box ml={-5}>
|
||||
@@ -41,7 +41,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
</Flex>
|
||||
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
<Flex align="center" className="cf-header-subtitle">
|
||||
<FeedFavicon url={props.entry.iconUrl} />
|
||||
<Space w={6} />
|
||||
<Box c="dimmed">
|
||||
@@ -51,7 +51,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
</Box>
|
||||
</Flex>
|
||||
{props.expanded && (
|
||||
<Box c="dimmed">
|
||||
<Box className="cf-header-details">
|
||||
{props.entry.author && <span>by {props.entry.author}</span>}
|
||||
{props.entry.author && props.entry.categories && <span> · </span>}
|
||||
{props.entry.categories && <span>{props.entry.categories}</span>}
|
||||
|
||||
@@ -42,11 +42,14 @@ function HeaderToolbar(props: { children: React.ReactNode }) {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
className="cf-toolbar"
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
) : (
|
||||
<Group gap={spacing}>{props.children}</Group>
|
||||
<Group gap={spacing} className="cf-toolbar">
|
||||
{props.children}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,7 +78,7 @@ export function Header() {
|
||||
|
||||
if (!settings) return <Loader />
|
||||
return (
|
||||
<Center>
|
||||
<Center className="cf-toolbar-wrapper">
|
||||
<HeaderToolbar>
|
||||
<ActionButton
|
||||
icon={<TbArrowUp size={iconSize} />}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Group, Stack } from "@mantine/core"
|
||||
import { Anchor, Box, Button, Group, Stack } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Alert } from "components/Alert"
|
||||
@@ -57,13 +58,27 @@ export function CustomCodeSettings() {
|
||||
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
|
||||
<Stack>
|
||||
<CodeEditor
|
||||
description={<Trans>Custom CSS rules that will be applied</Trans>}
|
||||
label={<Trans>Custom CSS rules that will be applied</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
<span>See </span>
|
||||
<Anchor
|
||||
href={Constants.customCssDocumentationUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ fontSize: "inherit" }}
|
||||
>
|
||||
here
|
||||
</Anchor>
|
||||
<span> for more information.</span>
|
||||
</Trans>
|
||||
}
|
||||
language="css"
|
||||
{...form.getInputProps("customCss")}
|
||||
/>
|
||||
|
||||
<CodeEditor
|
||||
description={<Trans>Custom JS code that will be executed on page load</Trans>}
|
||||
label={<Trans>Custom JS code that will be executed on page load</Trans>}
|
||||
language="javascript"
|
||||
{...form.getInputProps("customJs")}
|
||||
/>
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
redirectToTagDetails,
|
||||
} from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { TreeSubscription } from "app/tree/slice"
|
||||
import { collapseTreeCategory } from "app/tree/thunks"
|
||||
import type { Category, Subscription } from "app/types"
|
||||
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
||||
import { categoryHasNewEntries, categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
||||
import { Loader } from "components/Loader"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import React from "react"
|
||||
@@ -89,6 +90,7 @@ export function Tree() {
|
||||
name={<Trans>All</Trans>}
|
||||
icon={allIcon}
|
||||
unread={categoryUnreadCount(root)}
|
||||
hasNewEntries={categoryHasNewEntries(root)}
|
||||
selected={source.type === "category" && source.id === Constants.categories.all.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
@@ -103,6 +105,7 @@ export function Tree() {
|
||||
name={<Trans>Starred</Trans>}
|
||||
icon={starredIcon}
|
||||
unread={0}
|
||||
hasNewEntries={false}
|
||||
selected={source.type === "category" && source.id === Constants.categories.starred.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
@@ -122,6 +125,7 @@ export function Tree() {
|
||||
name={category.name}
|
||||
icon={category.expanded ? expandedIcon : collapsedIcon}
|
||||
unread={categoryUnreadCount(category)}
|
||||
hasNewEntries={categoryHasNewEntries(category)}
|
||||
selected={source.type === "category" && source.id === category.id}
|
||||
expanded={category.expanded}
|
||||
level={level}
|
||||
@@ -133,7 +137,7 @@ export function Tree() {
|
||||
)
|
||||
}
|
||||
|
||||
const feedNode = (feed: Subscription, level = 0) => {
|
||||
const feedNode = (feed: TreeSubscription, level = 0) => {
|
||||
if (!isFeedDisplayed(feed)) return null
|
||||
|
||||
return (
|
||||
@@ -143,6 +147,7 @@ export function Tree() {
|
||||
name={feed.name}
|
||||
icon={feed.iconUrl}
|
||||
unread={feed.unread}
|
||||
hasNewEntries={!!feed.hasNewEntries}
|
||||
selected={source.type === "feed" && source.id === String(feed.id)}
|
||||
level={level}
|
||||
hasError={feed.errorCount > errorThreshold}
|
||||
@@ -159,6 +164,7 @@ export function Tree() {
|
||||
name={tag}
|
||||
icon={tagIcon}
|
||||
unread={0}
|
||||
hasNewEntries={false}
|
||||
selected={source.type === "tag" && source.id === tag}
|
||||
level={0}
|
||||
hasError={false}
|
||||
@@ -182,7 +188,7 @@ export function Tree() {
|
||||
<OnDesktop>
|
||||
<TreeSearch feeds={feeds} />
|
||||
</OnDesktop>
|
||||
<Box>
|
||||
<Box className="cf-tree">
|
||||
{allCategoryNode()}
|
||||
{starredCategoryNode()}
|
||||
{root.children.map(c => recursiveCategoryNode(c))}
|
||||
|
||||
@@ -15,6 +15,7 @@ interface TreeNodeProps {
|
||||
expanded?: boolean
|
||||
level: number
|
||||
hasError: boolean
|
||||
hasNewEntries: boolean
|
||||
onClick: (e: React.MouseEvent, id: string) => void
|
||||
onIconClick?: (e: React.MouseEvent, id: string) => void
|
||||
}
|
||||
@@ -68,19 +69,19 @@ export function TreeNode(props: TreeNodeProps) {
|
||||
<Box
|
||||
py={1}
|
||||
pl={props.level * 20}
|
||||
className={classes.node}
|
||||
className={`${classes.node} cf-treenode cf-treenode-${props.type}`}
|
||||
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
|
||||
data-id={props.id}
|
||||
data-type={props.type}
|
||||
data-unread-count={props.unread}
|
||||
>
|
||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
|
||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)} className="cf-treenode-icon">
|
||||
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
|
||||
</Box>
|
||||
<Box className={classes.nodeText}>{props.name}</Box>
|
||||
{!props.expanded && (
|
||||
<Box>
|
||||
<UnreadCount unreadCount={props.unread} />
|
||||
<Box className="cf-treenode-unread-count">
|
||||
<UnreadCount unreadCount={props.unread} showIndicator={props.hasNewEntries} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { TextInput } from "@mantine/core"
|
||||
import { Box, TextInput } from "@mantine/core"
|
||||
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
@@ -33,7 +33,7 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
useMousetrap("g u", () => spotlight.open())
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box className="cf-treesearch">
|
||||
<TextInput
|
||||
placeholder={_(msg`Search`)}
|
||||
leftSection={searchIcon}
|
||||
@@ -58,6 +58,6 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
}}
|
||||
nothingFound={<Trans>Nothing found</Trans>}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge, Tooltip } from "@mantine/core"
|
||||
import { Badge, Indicator, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { tss } from "tss"
|
||||
|
||||
@@ -10,7 +10,7 @@ const useStyles = tss.create(() => ({
|
||||
},
|
||||
}))
|
||||
|
||||
export function UnreadCount(props: { unreadCount: number }) {
|
||||
export function UnreadCount(props: { unreadCount: number; showIndicator: boolean }) {
|
||||
const { classes } = useStyles()
|
||||
|
||||
if (props.unreadCount <= 0) return null
|
||||
@@ -18,9 +18,11 @@ export function UnreadCount(props: { unreadCount: number }) {
|
||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||
return (
|
||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||
<Badge className={classes.badge} variant="light" fullWidth>
|
||||
{count}
|
||||
</Badge>
|
||||
<Indicator disabled={!props.showIndicator} size={4} offset={10} position="middle-start">
|
||||
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
|
||||
{count}
|
||||
</Badge>
|
||||
</Indicator>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,28 +8,28 @@ interface Step {
|
||||
}
|
||||
|
||||
export const useAppLoading = () => {
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const profileLoaded = useAppSelector(state => !!state.user.profile)
|
||||
const settingsLoaded = useAppSelector(state => !!state.user.settings)
|
||||
const rootCategoryLoaded = useAppSelector(state => !!state.tree.rootCategory)
|
||||
const tagsLoaded = useAppSelector(state => !!state.user.tags)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: _(msg`Loading settings...`),
|
||||
done: !!settings,
|
||||
done: settingsLoaded,
|
||||
},
|
||||
{
|
||||
label: _(msg`Loading profile...`),
|
||||
done: !!profile,
|
||||
done: profileLoaded,
|
||||
},
|
||||
{
|
||||
label: _(msg`Loading subscriptions...`),
|
||||
done: !!rootCategory,
|
||||
done: rootCategoryLoaded,
|
||||
},
|
||||
{
|
||||
label: _(msg`Loading tags...`),
|
||||
done: !!tags,
|
||||
done: tagsLoaded,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0> هل تحتاج إلى حساب؟ </0> <1> اشترك! </ 1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>Ei,</0><1> sóc la Jérémie de Bèlgica i fa més de 10 anys que tre
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Necessites un compte?</0><1>Registreu-vos!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Potřebujete účet?</0><1>Zaregistrujte se!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Angen cyfrif?</0><1>Ymunwch!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Har du brug for en konto?</0><1>Tilmeld dig!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>Hey,</0><1>Ich bin Jérémie aus Belgien und arbeite seit über 10 Ja
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaF
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Need an account?</0><1>Sign up!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -34,6 +34,10 @@ msgstr "<0>Hola,</0><1>Soy Jérémie de Bélgica y he estado trabajando en Comma
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>¿Necesitas una cuenta?</0><1>¡Regístrate!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>به یک حساب نیاز دارید؟</0><1>ثبت نام کنید!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Tarvitsetko tilin?</0><1>Rekisteröidy!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>Salut,</0><1>Je m'appelle Jérémie, je suis belge, et je développe
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Besoin d'un compte ?</0><1>Enregistrez-vous !</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Necesitas unha conta?</0><1>Rexístrate!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Fiókra van szüksége?</0><1>Regisztráljon!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Butuh akun?</0><1>Daftar!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Hai bisogno di un account?</0><1>Registrati!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>こんにちは、</0><1>私はベルギーのジェレミーです
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>アカウントが必要ですか?</0><1>サインアップ!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>계정이 필요하십니까?</0><1>가입하세요!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Perlukan akaun?</0><1>Daftar!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Een account nodig?</0><1>Meld je aan!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Trenger du en konto?</0><1>Registrer deg!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Potrzebujesz konta?</0><1>Zarejestruj się!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Precisa de uma conta?</0><1>Inscreva-se!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>Здравствуйте,</0><1>Я Жереми из Бельгии,
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Нужен аккаунт?</0><1>Зарегистрируйтесь!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Potrebujete účet?</0><1>Zaregistrujte sa!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Behöver du ett konto?</0><1>Registrera dig!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>Merhaba,</0><1>Ben Belçika'dan Jérémie ve 10 yıldır boş zamanla
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Bir hesaba mı ihtiyacınız var?</0><1>Kaydolun!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
|
||||
@@ -33,6 +33,10 @@ msgstr "<0>您好,</0><1>我是来自比利时的Jérémie,已经在业余时
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>需要一个帐户?</0><1>注册!</1>"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "<0>See </0><1>here</1><2> for more information.</2>"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
@@ -389,7 +393,7 @@ msgstr "过滤表达式"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Font size"
|
||||
msgstr ""
|
||||
msgstr "字号"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
|
||||
@@ -42,7 +42,9 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
||||
const { id = Constants.categories.all.id } = useParams()
|
||||
const viewport = useViewportSize()
|
||||
const theme = useMantineTheme()
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const noSubscriptions = useAppSelector(
|
||||
state => state.tree.rootCategory && flattenCategoryTree(state.tree.rootCategory).every(c => c.feeds.length === 0)
|
||||
)
|
||||
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
||||
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
|
||||
const hasMore = useAppSelector(state => state.entries.hasMore)
|
||||
@@ -83,12 +85,11 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
||||
return () => promise.abort()
|
||||
}, [dispatch, props.sourceType, id, location.state?.timestamp])
|
||||
|
||||
const noSubscriptions = rootCategory && flattenCategoryTree(rootCategory).every(c => c.feeds.length === 0)
|
||||
if (noSubscriptions) return <NoSubscriptionHelp />
|
||||
return (
|
||||
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
|
||||
<Box mb={viewport.height * 0.7}>
|
||||
<Group gap="xl">
|
||||
<Group gap="xl" className="cf-entries-title">
|
||||
{sourceWebsiteUrl && (
|
||||
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
|
||||
<Title order={3}>{title}</Title>
|
||||
|
||||
@@ -35,9 +35,16 @@ interface LayoutProps {
|
||||
function LogoAndTitle() {
|
||||
const dispatch = useAppDispatch()
|
||||
return (
|
||||
<Center inline onClick={async () => await dispatch(redirectToRootCategory())} style={{ cursor: "pointer" }}>
|
||||
<Logo size={24} />
|
||||
<Title order={3} pl="md">
|
||||
<Center
|
||||
className="cf-logo-title"
|
||||
inline
|
||||
onClick={async () => await dispatch(redirectToRootCategory())}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Box className="cf-logo">
|
||||
<Logo size={24} />
|
||||
</Box>
|
||||
<Title order={3} pl="md" className="cf-title">
|
||||
CommaFeed
|
||||
</Title>
|
||||
</Center>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default defineConfig(() => ({
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: ["@lingui/babel-plugin-lingui-macro"],
|
||||
plugins: [["babel-plugin-react-compiler", { target: "19" }], "@lingui/babel-plugin-lingui-macro"],
|
||||
},
|
||||
}),
|
||||
lingui(),
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>5.8.0</version>
|
||||
<version>5.10.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
|
||||
<properties>
|
||||
<quarkus.version>3.22.1</quarkus.version>
|
||||
<quarkus.version>3.22.3</quarkus.version>
|
||||
<querydsl.version>6.11</querydsl.version>
|
||||
<rome.version>2.1.0</rome.version>
|
||||
<swagger.version>2.2.30</swagger.version>
|
||||
<swagger.version>2.2.32</swagger.version>
|
||||
|
||||
<build.database>h2</build.database>
|
||||
</properties>
|
||||
@@ -221,7 +221,7 @@
|
||||
<plugin>
|
||||
<groupId>io.github.git-commit-id</groupId>
|
||||
<artifactId>git-commit-id-maven-plugin</artifactId>
|
||||
<version>9.0.1</version>
|
||||
<version>9.0.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -269,7 +269,7 @@
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>10.23.1</version>
|
||||
<version>10.24.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
@@ -327,7 +327,7 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>5.8.0</version>
|
||||
<version>5.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- compile-time processors -->
|
||||
@@ -391,7 +391,7 @@
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-json</artifactId>
|
||||
<version>4.2.30</version>
|
||||
<version>4.2.32</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
@@ -483,7 +483,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.4.4</version>
|
||||
<version>5.5</version>
|
||||
</dependency>
|
||||
<!-- add brotli support for httpclient5 -->
|
||||
<dependency>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ibm-semeru-runtimes:open-21.0.6_7-jre@sha256:bb59a7eb5bfab35cb112c403521312079a8fcfb85e9e3ca9afe8fc24ee57ae94
|
||||
FROM ibm-semeru-runtimes:open-21.0.7_6-jre@sha256:81475a86a6abb3b62feae30112f13f7da20b8c6968eba0d3e7c76016200e6b56
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:12.10@sha256:264982ff4d18000fa74540837e2c43ca5137a53a83f8f62c7b3803c0f0bdcd56
|
||||
FROM debian:12.11@sha256:bd73076dc2cd9c88f48b5b358328f24f2a4289811bd73787c031e20db9f97123
|
||||
ARG TARGETARCH
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
@@ -151,7 +151,7 @@ public class HttpGetter {
|
||||
}
|
||||
|
||||
private void ensureHttpScheme(String scheme) throws SchemeNotAllowedException {
|
||||
if (!"http".equals(scheme) && !"https".equals(scheme)) {
|
||||
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
|
||||
throw new SchemeNotAllowedException(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ quarkus.http.test-port=8085
|
||||
quarkus.http.enable-compression=true
|
||||
|
||||
# http cache
|
||||
quarkus.http.static-resources.max-age=P365d
|
||||
## make sure the webapp is always up to date
|
||||
quarkus.http.filter.index-html.header."Cache-Control"=no-cache
|
||||
quarkus.http.filter.index-html.matches=/
|
||||
@@ -37,6 +38,8 @@ quarkus.shutdown.timeout=5s
|
||||
# native
|
||||
quarkus.native.march=compatibility
|
||||
quarkus.native.add-all-charsets=true
|
||||
# fix for https://github.com/Athou/commafeed/issues/1795
|
||||
quarkus.native.additional-build-args=-H:PageSize=65536
|
||||
|
||||
|
||||
# dev profile overrides
|
||||
|
||||
@@ -18,6 +18,6 @@ class StaticFilesIT {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "/favicon.ico" })
|
||||
void servedWithCache(String path) {
|
||||
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=86400");
|
||||
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=31536000");
|
||||
}
|
||||
}
|
||||
|
||||
121
documentation/CUSTOMCSS.md
Normal file
121
documentation/CUSTOMCSS.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Custom CSS Guide
|
||||
|
||||
On the Admin settings page, there is a tab for "Custom Code" where you can enter [CSS](https://en.wikipedia.org/wiki/CSS) to customize the look & feel of CommaFeed. Many of the HTML elements of CommaFeed have been given specific class names to make it easier to write CSS rules. For example, the header of a feed entry is enclosed in an HTML `<div>` element with the class name `cf-header`. So if you wanted to modify the appearance of all feed entry headers, you could use that specific class name to select all feed entry headers and apply whatever changes you desired. For example, if you want to make change the background color of the headers, you could add this to the custom CSS code:
|
||||
|
||||
```
|
||||
.cf-header {background-color: lightblue;}
|
||||
```
|
||||
|
||||
The tables below list useful page elements as well as all of the CommaFeed specific class names, and describes their corresponding page elements. These elements are selected to provide a good "starting point" within each portion of the feed reading page. Sometimes that's all you'll need, but to modify some elements of the page, you will have to start at one of the CommaFeed specific class names and then "drill down" to the HTML element you wish to change. A useful approach is to use your web browser's Inspector to find the element you want to change. (Typically you can hit F12 to bring up the web developer tools.) Find the element you want to change. If it has a "cf-" class name from the table below, then you can modify it directly using the formula illustrated above with "cf-header". If it doesn't have a "cf-" classname, then search upwards through the enclosing elements until you find one that does. Use that name as your starting point and extend your CSS selector down to the element you want to modify.
|
||||
|
||||
If you're having trouble writing a CSS rule, changing the background color as in the rule above can be helpful to see what elements your selector is selecting -- or if it is not selecting anything!
|
||||
|
||||
## Extended Example
|
||||
The extended example below modifies the CommaFeed interface to be more minimal and illustrates a variety of uses of the CommaFeed class names and CSS. There are examples of styling CommaFeed classes directly as well as examples of more complicated selection rules.
|
||||
|
||||
```
|
||||
/* GENERAL (changes applied to everything) */
|
||||
main {font-size: 14px; font-family: sans-serif; line-height: 1.35; padding-top: calc(1rem * 2.5) !important;}
|
||||
/* Don't force font-size on blockquotes and make them italic */
|
||||
blockquote {font-size: unset !important; font-style: italic;}
|
||||
/* Make all the button icons black */
|
||||
header svg {stroke: black !important; }
|
||||
main > svg {stroke: black !important; }
|
||||
/* Make links in articles light blue with a hover underline */
|
||||
article a:not([class]) { color:#428bca; text-decoration:none; }
|
||||
article a:not([class]):hover { text-decoration:underline; }
|
||||
/* Make HTML headers the (same) reasonable size */
|
||||
h3 {font-size: 16px !important;}
|
||||
h2 {font-size: 16px !important;}
|
||||
h1 {font-size: 16px !important;}
|
||||
/* Make buttons actual size */
|
||||
main > button {min-width: unset !important; min-height: unset !important;}
|
||||
|
||||
/* HEADER (tool bar at the top of the page) */
|
||||
/* Make the header more compact */
|
||||
header > div > div {padding-bottom: 0 !important; padding-top: 0 !important;}
|
||||
/* Let the toolbar pull to the left */
|
||||
.cf-toolbar-wrapper {justify-content: unset !important;}
|
||||
/* Minimize height of the toolbar */
|
||||
header {height: unset !important;}
|
||||
/* Move buttons closer together */
|
||||
header img {width: calc(1rem) !important;}
|
||||
/* No button labels, even if there's room. */
|
||||
.cf-toolbar-wrapper .mantine-Button-label {display: none;}
|
||||
|
||||
/* SIDEBAR (where the feeds are listed) */
|
||||
/* Specific font and layout changes for the entire sidebar */
|
||||
.cf-tree {font-size: 14px; font-weight: 700; font-family: sans-serif; line-height: 150%; top: 30px !important;}
|
||||
.cf-treenode {margin-right: 0}
|
||||
/* Make unread category names black */
|
||||
.cf-treenode-category {color: black !important;}
|
||||
/* Remove the favicons for the feeds in the sidebar */
|
||||
.cf-treenode-icon {display: none;}
|
||||
/* Make the unread counts lighter, gray and in parens */
|
||||
.cf-badge {display: flex; font-weight: 300; color: gray; background-color: unset; align-items: unset;}
|
||||
.cf-badge::before {content: "(";}
|
||||
.cf-badge::after {content: ")";}
|
||||
|
||||
/* FEED ENTRIES */
|
||||
/* Only changes Detailed and Expanded display */
|
||||
/* Remove subtitle and details in feed entries, just leaving the title */
|
||||
.cf-header-subtitle {display: none;}
|
||||
.cf-header-details {display: none;}
|
||||
/* Remove the divider and button bar at the bottom of feed entries */
|
||||
.cf-footer-divider {display: none;}
|
||||
.cf-footer {display: none;}
|
||||
|
||||
/* MISCELLANEOUS */
|
||||
/* An example of changing the content: Add an extra space before the submitted line on Reddit feed entries. */
|
||||
article span > div::after {content: "\A"; white-space: pre;}
|
||||
```
|
||||
|
||||
## CommaFeed Useful Elements
|
||||
The table below shows some elements of the CommaFeed main page that are useful for applying custom CSS. Note that these are elements, not class names, so you must use these without the leading period used to reference a class name. For example:
|
||||
|
||||
```
|
||||
article {background-color: lightblue;}
|
||||
```
|
||||
|
||||
|Element Name|Element Description|
|
||||
|---|---|
|
||||
|main|The entire web page|
|
||||
|header|The header area (logo and toolbar)|
|
||||
|nav|The entire sidebar|
|
||||
|footer|The footer area at the bottom of the page|
|
||||
|article|Entire feed entry|
|
||||
|h3, h2, h1|HTML headers|
|
||||
|
||||
|
||||
## CommaFeed Class Names
|
||||
The table below shows the CommaFeed specific class names. To reference a class name in a CSS rule, use a leading period. For example:
|
||||
|
||||
```
|
||||
.cf-header {background-color: lightblue;}
|
||||
```
|
||||
|
||||
|Class Name|Element Description|
|
||||
|---|---|
|
||||
|cf-logo-title|The CommaFeed logo and title in upper left of page|
|
||||
|cf-logo|The CommaFeed logo|
|
||||
|cf-title|The CommaFeed title|
|
||||
|cf-toolbar|The entire toolbar of action buttons at the top of the page|
|
||||
|cf-action-button|Each button within the toolbar. (Note: also used in feed entry footer.)|
|
||||
|cf-treesearch|The search box at the top of the sidebar|
|
||||
|cf-tree|The entire feed tree in the sidebar|
|
||||
|cf-treenode|All nodes in the feed tree|
|
||||
|cf-treenode-category|Category nodes in the feed tree|
|
||||
|cf-treenode-feed|Feed nodes in the feed tree|
|
||||
|cf-treenode-icon|Icon within feed nodes|
|
||||
|cf-treenode-unread-count|Unread count within feed nodes|
|
||||
|cf-badge|The badge for the unread count|
|
||||
|cf-entries-title|Title of feed currently displayed in the content area|
|
||||
|cf-entries|All of the feed entries being displayed in the content area|
|
||||
|cf-header|The header of a feed entry|
|
||||
|cf-header-title|The first line in the header of a feed entry (the entry title)|
|
||||
|cf-header-subtitle|The second line in the header of a feed entry (feed name and time of entry)|
|
||||
|cf-header-details|The third line in the header of a feed entry (typically author, subject, etc.)|
|
||||
|cf-content|The content (body) of a feed entry|
|
||||
|cf-footer-divider|The divider between the feed entry content and the feed entry footer|
|
||||
|cf-footer|The feed entry footer (buttons to share, star, etc.)|
|
||||
|cf-action-button|Each button within the feed entry footer. (note: also used in toolbar.)|
|
||||
Reference in New Issue
Block a user