68 Commits
Author SHA1 Message Date
ZSCGR 819640c683 解决与原作者代码的冲突 2025-11-23 00:51:46 +08:00
ourongxing fcdf837435 chore: release v0.0.37 2025-11-20 10:14:46 +00:00
ourongxing fa9f552d73 fix: kaopu source 2025-11-20 10:06:54 +00:00
ourongxing 7e88e142b2 fix: weibo 2025-11-20 09:59:51 +00:00
likanugandGitHub b60bb219ec feat: Add new source: 腾讯新闻-综合早报 (#255) 2025-11-20 00:28:55 +08:00
ourongxing 3e870e1e37 chore: remove autorefresh script 2025-11-01 10:09:07 +00:00
ourongxing 3721fd383a feat(source): add steam 2025-10-27 16:58:16 +00:00
ourongxing 8142074b3c chore: release v0.0.36 2025-10-27 16:25:24 +00:00
ourongxing 5a2e2a5e9c chore: minor changes 2025-10-27 16:20:57 +00:00
ourongxing cb9b320996 feat(docker): add PRODUCTHUNT_API_TOKEN environment variable 2025-10-27 16:18:30 +00:00
ourongxing 3b845ce6b5 fix(source): mktnews 2025-10-27 16:14:01 +00:00
ourongxing a0941eff63 fix(source): producthunt by offical api 2025-10-27 16:10:04 +00:00
ourongxing 7e6805ca87 chore(source): opt display style 2025-10-27 15:51:58 +00:00
likanugandGitHub 4b89a57889 feat: Add new source: 豆瓣热门电影 (#237)
* feat: Add new source 豆瓣热门电影

* refactor: Simplify the if condition
2025-10-27 23:38:59 +08:00
5627cbef60 fix: kaopu source (#238)
Co-authored-by: kanson <[email protected]>
2025-10-27 23:38:39 +08:00
ZSCGRandGitHub 57dc2c319c Update sources.json 2025-09-28 18:47:55 +08:00
ZSCGRandGitHub 84543b12b6 Update idcflare.ts 2025-09-22 20:38:02 +08:00
ZSCGRandGitHub 3584efb09b Update idcflare.ts 2025-09-21 21:49:31 +08:00
ZSCGRandGitHub f357e920b7 Update weibo.ts 2025-09-21 21:43:09 +08:00
ZSCGRandGitHub dca03c4f0c Update refresh.ts 2025-09-21 21:42:34 +08:00
ZSCGRandGitHub 576137a62f Update glob.d.ts 2025-09-21 21:31:28 +08:00
ZSCGRandGitHub a896f15c44 Update pinyin.json 2025-09-21 21:11:19 +08:00
ZSCGRandGitHub f78eead5fd Create idcflare.ts 2025-09-21 21:07:18 +08:00
ZSCGRandGitHub 6fe8f03f16 Update sources.json 2025-09-21 21:04:38 +08:00
ZSCGRandGitHub 8e7b35385b Update pre-sources.ts 2025-09-21 21:03:29 +08:00
ourongxing f2698ba2f0 chore: release v0.0.35 2025-09-17 14:01:08 +08:00
ourongxing 5a07c02ad0 fix: adjust layout in NewsListHot component for better alignment 2025-09-17 14:00:40 +08:00
ourongxing ce0baddc78 fix: weibo source 2025-09-17 13:44:19 +08:00
ZSCGRandGitHub 0271066e27 Update pre-sources.ts 2025-08-19 14:03:18 +08:00
ZSCGRandGitHub f2e4ce36de Update pre-sources.ts 2025-08-19 13:55:57 +08:00
ZSCGRandGitHub cce09de022 Update sources.json 2025-08-19 13:45:58 +08:00
ZSCGRandGitHub 9e37d4449f Update sources.json 2025-08-19 13:42:39 +08:00
ZSCGRandGitHub aff39c1e1b Update pinyin.json 2025-08-19 13:29:29 +08:00
ZSCGRandGitHub 6c22566079 Update pinyin.json 2025-08-18 22:48:38 +08:00
ZSCGRandGitHub cfb48a8c25 Update sources.json 2025-08-18 22:24:58 +08:00
ZSCGRandGitHub 0580495208 Update docker.yml 2025-08-18 21:41:02 +08:00
ZSCGRandGitHub a914396685 Update linuxdo.ts 2025-08-18 21:09:59 +08:00
ourongxing 51fa15a4e4 chore: release v0.0.34 2025-08-15 16:36:58 +08:00
ourongxing 3968f658ee chore: update dependencies 2025-08-15 16:36:45 +08:00
521141andGitHub c4c287a40d revert: Remove MySQL support and revert to pre-modification state (#226) 2025-08-15 16:31:40 +08:00
ourongxing ae37735391 fix: show mobile scrolling instruction only on mobile 2025-08-15 01:10:57 +08:00
ourongxing c16797cb59 chore: release v0.0.33 2025-08-13 00:49:31 +08:00
ourongxing 3aa2b91b05 feat: tweak the height of the mobile search modal box 2025-08-13 00:34:35 +08:00
ourongxing 1eda9d52b6 feat: horizontal scrolling on mobile devices
closed #215
2025-08-13 00:33:14 +08:00
f8579bbe4f fix: mysql in docker (#216)
* fix(database): Implement dynamic database connector selection

* refactor: Convert database detection messages to English

* chore: add mysql2 dependency

* Update docker-compose.yml

---------

Co-authored-by: Ou <[email protected]>
2025-08-12 23:21:35 +08:00
ourongxing d1fc4f1410 chore: disable light mode for it's ugly. 2025-08-11 23:23:36 +08:00
ourongxing e9804b9ee2 chore: add github button 2025-08-11 23:19:04 +08:00
ourongxing 640a4550d6 chore: release v0.0.32 2025-08-07 10:20:54 +08:00
ourongxing fc57acf9cc fix: weibo 2025-08-06 20:26:39 +08:00
chenxuanandourongxing 681d137975 fix: Using the hupu official API (#203) 2025-08-06 20:26:39 +08:00
ourongxing ca7d6475f9 chore: update README 2025-07-23 21:08:33 +08:00
ourongxing 27c623d779 docs: update README 2025-07-17 23:26:05 +08:00
ourongxing 3d7026611b chore: release v0.0.31 2025-07-17 19:46:55 +08:00
521141andGitHub bb5fad47cb feat(database): add MySQL database support (#201) 2025-07-17 19:44:01 +08:00
ourongxing 0ee5ef316f docs: update README 2025-07-17 19:30:13 +08:00
ourongxing da3dabcba4 fix(source): filter type 'theme' in wallstreetcn source 2025-06-27 18:20:14 +08:00
ourongxing 332ed38540 fix: mkt news 2025-06-27 18:18:03 +08:00
Johnnyandourongxing 1d522f320c fix(ui): When title wraps, number border stretched (#188) 2025-06-27 17:59:27 +08:00
SleepSheepandGitHub 735a0bca82 feat: add MKTNews as a news source (#185) 2025-06-27 17:55:48 +08:00
ourongxing 0331caf1a6 chore: update gitignore 2025-06-22 12:08:37 +08:00
Ou c738c4c4cf chore: release v0.0.30 2025-05-11 00:55:52 +08:00
Ou 9f1be628e0 feat: support persistent data in docker, using docker volume 2025-05-11 00:55:41 +08:00
Ou 10f5d9a53d fix: docker image 2025-05-11 00:54:37 +08:00
Ou 67f33961d2 chore: release v0.0.29 2025-05-10 18:34:31 +08:00
Ou 1fb665be18 chore: uncomment feature code 2025-05-10 18:31:45 +08:00
Ou 7f9591f465 fix(source): zhihu
fixed #167
2025-05-10 15:20:53 +08:00
Ou b784389021 feat: add streamable http mcp server 2025-05-02 20:59:02 +08:00
Ou 2e32ca13a7 chore: update deps 2025-05-02 20:53:16 +08:00
48 changed files with 5694 additions and 3358 deletions
+3
View File
@@ -0,0 +1,3 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**
-23
View File
@@ -1,23 +0,0 @@
name: Auto Refresh
on:
schedule:
- cron: '*/20 0-17,22,23 * * *'
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Refresh
env:
JWT_TOKEN: ${{ secrets.JWT_TOKEN }}
run: npx tsx ./scripts/refresh.ts
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.GGITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
+2 -1
View File
@@ -12,4 +12,5 @@ dev-dist
*.tsbuildinfo
wrangler.toml
imports.app.d.ts
package-lock.json
package-lock.json
.specstory/
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20.12.2-alpine as builder
FROM node:20.12.2-alpine AS builder
WORKDIR /usr/src
COPY . .
RUN corepack enable
-2
View File
@@ -1,5 +1,3 @@
# NewsNow
![](/public/og-image.png)
[English](./README.md) | [简体中文](README.zh-CN.md) | 日本語
-2
View File
@@ -1,5 +1,3 @@
# NewsNow
![](/public/og-image.png)
English | [简体中文](README.zh-CN.md) | [日本語](README.ja-JP.md)
+2 -4
View File
@@ -1,7 +1,3 @@
# NewsNow
<a href="https://hellogithub.com/repository/c2978695e74a423189e9ca2543ab3b36" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=c2978695e74a423189e9ca2543ab3b36&claim_uid=SMJiFwlsKCkWf89&theme=small" alt="FeaturedHelloGitHub" /></a>
![](/public/og-image.png)
[English](./README.md) | 简体中文 | [日本語](README.ja-JP.md)
@@ -114,3 +110,5 @@ pnpm dev
如果本项目对你有所帮助,可以给小猫买点零食。如果需要定制或者其他帮助,请通过下列方式联系备注。
![](./screenshots/reward.gif)
<a href="https://hellogithub.com/repository/c2978695e74a423189e9ca2543ab3b36" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=c2978695e74a423189e9ca2543ab3b36&claim_uid=SMJiFwlsKCkWf89&theme=small" alt="FeaturedHelloGitHub" /></a>
+17
View File
@@ -0,0 +1,17 @@
services:
newsnow:
build: .
ports:
- '4444:4444'
volumes:
- newsnow_data:/usr/app/.data
environment:
- G_CLIENT_ID=
- G_CLIENT_SECRET=
- JWT_SECRET=
- INIT_TABLE=true
- ENABLE_CACHE=true
volumes:
newsnow_data:
name: newsnow_data
+10 -3
View File
@@ -1,15 +1,22 @@
version: '3'
services:
newsnow:
image: ghcr.io/ourongxing/newsnow:latest
container_name: newsnow
restart: always
ports:
- '4444:4444'
volumes:
- newsnow_data:/usr/app/.data
environment:
- HOST=0.0.0.0
- PORT=4444
- NODE_ENV=production
- G_CLIENT_ID=
- G_CLIENT_SECRET=
- JWT_SECRET=
- INIT_TABLE=true
- ENABLE_CACHE=true
- PRODUCTHUNT_API_TOKEN=
volumes:
newsnow_data:
name: newsnow_data
+2 -1
View File
@@ -2,4 +2,5 @@ G_CLIENT_ID=
G_CLIENT_SECRET=
JWT_SECRET=
INIT_TABLE=true
ENABLE_CACHE=true
ENABLE_CACHE=true
PRODUCTHUNT_API_TOKEN=
+1 -14
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="zh-CN">
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8" />
@@ -49,19 +49,6 @@
</script>
<script>
function safeParseString(str) {
try {
return JSON.parse(str)
} catch {
return ""
}
}
const theme = safeParseString(localStorage.getItem("color-scheme")) || "dark"
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches
if (theme !== "light") {
document.documentElement.classList.add("dark")
}
const query = new URLSearchParams(window.location.search)
if (query.has("login") && query.has("user") && query.has("jwt")) {
localStorage.setItem("user", query.get("user"))
+11 -1
View File
@@ -17,6 +17,11 @@ const nitroOption: Parameters<typeof viteNitro>[0] = {
connector: "better-sqlite3",
},
},
devDatabase: {
default: {
connector: "better-sqlite3",
},
},
imports: {
dirs: ["server/utils", "shared"],
},
@@ -38,6 +43,11 @@ if (process.env.VERCEL) {
// }
} else if (process.env.CF_PAGES) {
nitroOption.preset = "cloudflare-pages"
nitroOption.unenv = {
alias: {
"safer-buffer": "node:buffer",
},
}
nitroOption.database = {
default: {
connector: "cloudflare-d1",
@@ -57,4 +67,4 @@ if (process.env.VERCEL) {
export default function () {
return viteNitro(nitroOption)
}
}
+9 -5
View File
@@ -1,9 +1,9 @@
{
"name": "newsnow",
"type": "module",
"version": "0.0.28",
"version": "0.0.37",
"private": true,
"packageManager": "pnpm@10.5.2",
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
"author": {
"url": "https://github.com/ourongxing/",
"email": "[email protected]",
@@ -30,11 +30,12 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@formkit/auto-animate": "^0.8.2",
"@iconify-json/si": "^1.2.3",
"@modelcontextprotocol/sdk": "^1.11.0",
"@tanstack/react-query-devtools": "^5.66.11",
"@tanstack/react-router": "^1.112.0",
"@unocss/reset": "^66.0.0",
"ahooks": "^3.8.4",
"better-sqlite3": "^11.8.1",
"better-sqlite3": "^11.10.0",
"cheerio": "^1.0.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
@@ -54,6 +55,7 @@
"overlayscrollbars": "^2.11.1",
"pnpm": "^10.5.2",
"react": "^19.0.0",
"react-device-detect": "^2.2.3",
"react-dom": "^19.0.0",
"react-use": "^17.6.0",
"uncrypto": "^0.1.3",
@@ -99,7 +101,7 @@
"vitest": "^3.0.7",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0",
"wrangler": "^3.111.0"
"wrangler": "4.14.1"
},
"pnpm": {
"patchedDependencies": {
@@ -109,16 +111,18 @@
"@napi-rs/pinyin",
"@parcel/watcher",
"@swc/core",
"better-sqlite3",
"esbuild",
"better-sqlite3",
"sharp",
"simple-git-hooks",
"unrs-resolver",
"workerd"
]
},
"resolutions": {
"cross-spawn": ">=7.0.6",
"dayjs": "1.11.13",
"nitropack": "npm:[email protected]",
"picomatch": "^4.0.2",
"react": "^19",
"db0": "^0.3.1",
+4833 -3040
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

-5
View File
@@ -1,5 +0,0 @@
import sources from "../shared/sources.json"
Promise.all(Object.keys(sources).map(id =>
fetch(`https://newsnow.busiyi.world/api/s?id=${id}`),
)).catch(console.error)
+30
View File
@@ -0,0 +1,30 @@
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
import { getServer } from "#/mcp/server"
export default defineEventHandler(async (event) => {
const req = event.node.req
const res = event.node.res
const server = getServer()
try {
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
transport.onerror = console.error.bind(console)
await server.connect(transport)
await transport.handleRequest(req, res, await readBody(event))
res.on("close", () => {
// console.log("Request closed")
transport.close()
server.close()
})
return res
} catch (e) {
console.error(e)
return {
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
}
}
})
+1 -1
View File
@@ -82,4 +82,4 @@ export async function getCacheTable() {
} catch (e) {
logger.error("failed to init database ", e)
}
}
}
+1 -1
View File
@@ -66,4 +66,4 @@ export class UserTable {
if (!state.success) throw new Error(`delete user ${key} failed`)
logger.success(`delete user ${key}`)
}
}
}
+5
View File
@@ -8,6 +8,7 @@ declare module 'glob:./sources/{*.ts,**/index.ts}' {
export const chongbuluo: typeof import('./sources/chongbuluo')
export const cls: typeof import('./sources/cls/index')
export const coolapk: typeof import('./sources/coolapk/index')
export const douban: typeof import('./sources/douban')
export const douyin: typeof import('./sources/douyin')
export const fastbull: typeof import('./sources/fastbull')
export const gelonghui: typeof import('./sources/gelonghui')
@@ -22,6 +23,8 @@ declare module 'glob:./sources/{*.ts,**/index.ts}' {
export const kaopu: typeof import('./sources/kaopu')
export const kuaishou: typeof import('./sources/kuaishou')
export const linuxdo: typeof import('./sources/linuxdo')
export const idcflare: typeof import('./sources/idcflare')
export const mktnews: typeof import('./sources/mktnews')
export const nowcoder: typeof import('./sources/nowcoder')
export const pcbeta: typeof import('./sources/pcbeta')
export const producthunt: typeof import('./sources/producthunt')
@@ -29,6 +32,8 @@ declare module 'glob:./sources/{*.ts,**/index.ts}' {
export const solidot: typeof import('./sources/solidot')
export const sputniknewscn: typeof import('./sources/sputniknewscn')
export const sspai: typeof import('./sources/sspai')
export const steam: typeof import('./sources/steam')
export const tencent: typeof import('./sources/tencent')
export const thepaper: typeof import('./sources/thepaper')
export const tieba: typeof import('./sources/tieba')
export const toutiao: typeof import('./sources/toutiao')
+10
View File
@@ -0,0 +1,10 @@
import sources from "../../shared/sources.json"
export const description = Object.entries(sources).filter(([_, source]) => {
if (source.redirect) {
return false
}
return true
}).map(([id, source]) => {
return source.title ? `${source.name}-${source.title} id is ${id}` : `${source.name} id is ${id}`
}).join(";")
+44
View File
@@ -0,0 +1,44 @@
import { z } from "zod"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"
import packageJSON from "../../package.json"
import { description } from "./desc.js"
export function getServer() {
const server = new McpServer(
{
name: "NewsNow",
version: packageJSON.version,
},
{ capabilities: { logging: {} } },
)
server.tool(
"get_hotest_latest_news",
`get hotest or latest news from source by {id}, return {count: 10} news.`,
{
id: z.string().describe(`source id. e.g. ${description}`),
count: z.any().default(10).describe("count of news to return."),
},
async ({ id, count }): Promise<CallToolResult> => {
let n = Number(count)
if (Number.isNaN(n) || n < 1) {
n = 10
}
const res: SourceResponse = await $fetch(`/api/s?id=${id}`)
return {
content: res.items.slice(0, count).map((item) => {
return {
text: `[${item.title}](${item.url})`,
type: "text",
}
}),
}
},
)
server.server.onerror = console.error.bind(console)
return server
}
+1 -1
View File
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
if (!url.pathname.startsWith("/api")) return
if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) {
event.context.disabledLogin = true
if (["/api/s", "/api/proxy", "/api/latest"].every(p => !url.pathname.startsWith(p)))
if (["/api/s", "/api/proxy", "/api/latest", "/api/mcp"].every(p => !url.pathname.startsWith(p)))
throw createError({ statusCode: 506, message: "Server not configured, disable login" })
} else {
if (["/api/s", "/api/me"].find(p => url.pathname.startsWith(p))) {
+47
View File
@@ -0,0 +1,47 @@
interface HotMoviesRes {
category: string
tags: []
items: MovieItem[]
recommend_tags: []
total: number
type: string
}
interface MovieItem {
rating: {
count: number
max: number
star_count: number
value: number
}
title: string
pic: {
large: string
normal: string
}
is_new: boolean
uri: string
episodes_info: string
card_subtitle: string
type: string
id: string
}
export default defineSource(async () => {
const baseURL = "https://m.douban.com/rexxar/api/v2/subject/recent_hot/movie"
const res: HotMoviesRes = await myFetch(baseURL, {
headers: {
Referer: "https://movie.douban.com/",
Accept: "application/json, text/plain, */*",
},
})
return res.items.map(movie => ({
id: movie.id,
title: movie.title,
url: `https://movie.douban.com/subject/${movie.id}`,
extra: {
info: movie.card_subtitle.split(" / ").slice(0, 3).join(" / "),
hover: movie.card_subtitle,
},
}))
})
+33 -16
View File
@@ -1,20 +1,37 @@
interface Res {
data: {
title: string
hot: string
url: string
mobil_url: string
}[]
interface HotItem {
id: string
title: string
url: string
mobileUrl: string
}
export default defineSource(async () => {
const r: Res = await myFetch(`https://api.vvhan.com/api/hotlist/huPu`)
return r.data.map((k) => {
return {
id: k.url,
title: k.title,
url: k.url,
mobileUrl: k.mobil_url,
}
})
// 获取虎扑新热榜页面的HTML内容
const html = await myFetch(`https://bbs.hupu.com/topic-daily-hot`)
// 正则表达式匹配新的热榜项结构
const regex = /<li class="bbs-sl-web-post-body">[\s\S]*?<a href="(\/[^"]+?\.html)"[^>]*?class="p-title"[^>]*>([^<]+)<\/a>/g
const result: HotItem[] = []
let match
// 将赋值操作移到循环内部,修复no-cond-assign警告
while (true) {
match = regex.exec(html)
if (!match) break
const [, path, title] = match
// 构建完整URL
const url = `https://bbs.hupu.com${path}`
result.push({
id: path,
title: title.trim(),
url,
mobileUrl: url,
})
}
return result
})
+61
View File
@@ -0,0 +1,61 @@
interface Res {
topic_list: {
can_create_topic: boolean
more_topics_url: string
per_page: number
top_tags: string[]
topics: {
id: number
title: string
fancy_title: string
posts_count: number
reply_count: number
highest_post_number: number
image_url: null | string
created_at: Date
last_posted_at: Date
bumped: boolean
bumped_at: Date
unseen: boolean
pinned: boolean
excerpt?: string
visible: boolean
closed: boolean
archived: boolean
like_count: number
has_summary: boolean
last_poster_username: string
category_id: number
pinned_globally: boolean
}[]
}
}
const hot = defineSource(async () => {
const res = await myFetch<Res>("https://n8n.chgr.cc/webhook/api?name=idcflare")
return res.topic_list.topics
.filter(k => k.visible && !k.archived && !k.pinned)
.map(k => ({
id: k.id,
title: k.title,
url: `https://idcflare.com/t/topic/${k.id}`,
}))
})
const latest = defineSource(async () => {
const res = await myFetch<Res>("https://idcflare.com/latest.json?order=created")
return res.topic_list.topics
.filter(k => k.visible && !k.archived && !k.pinned)
.map(k => ({
id: k.id,
title: k.title,
pubDate: new Date(k.created_at).valueOf(),
url: `https://idcflare.com/t/topic/${k.id}`,
}))
})
export default defineSource({
"idcflare": latest,
"idcflare-latest": latest,
"idcflare-hot": hot,
})
+4 -4
View File
@@ -2,17 +2,17 @@ type Res = {
description: string
link: string
// Date
pubDate: string
pub_date: string
publisher: string
title: string
}[]
export default defineSource(async () => {
const res = await Promise.all(["https://kaopucdn.azureedge.net/jsondata/news_list_beta_hans_0.json", "https://kaopucdn.azureedge.net/jsondata/news_list_beta_hans_1.json"].map(url => myFetch(url) as Promise<Res>))
return res.flat().filter(k => ["财新", "公视"].every(h => k.publisher !== h)).map((k) => {
const res: Res = await $fetch("https://kaopustorage.blob.core.windows.net/news-prod/news_list_hans_0.json")
return res.filter(k => ["财新", "公视"].every(h => k.publisher !== h)).map((k) => {
return {
id: k.link,
title: k.title,
pubDate: k.pubDate,
pubDate: k.pub_date,
extra: {
hover: k.description,
info: k.publisher,
+1 -1
View File
@@ -32,7 +32,7 @@ interface Res {
}
const hot = defineSource(async () => {
const res = await myFetch<Res>("https://linux.do/top/daily.json")
const res = await myFetch<Res>("https://n8n.chgr.cc/webhook/api?name=linuxdo")
return res.topic_list.topics
.filter(k => k.visible && !k.archived && !k.pinned)
.map(k => ({
+66
View File
@@ -0,0 +1,66 @@
interface Report {
id: string
type: number
time: string
important: number
data: {
content: string
pic: string
title: string
}
remark: string[]
hot: boolean
hot_start: string
hot_end: string
classify: {
id: number
pid: number
name: string
parent: string
}[]
}
interface Res {
data: {
id: number
name: string
pid: number
child: {
id: number
name: string
pid: number
flash_list: Report[]
}[]
}[]
}
const flash = defineSource(async () => {
const res: Res = await myFetch("https://api.mktnews.net/api/flash/host")
const categories = ["policy", "AI", "financial"] as const
const typeMap = { policy: "Policy", AI: "AI", financial: "Financial" } as const
const allReports = categories.flatMap((category) => {
const categoryData = res.data.find(item => item.name === category)
if (!categoryData?.child) return []
return categoryData.child.flatMap(subCategory =>
(subCategory.flash_list || []).map(item => ({ ...item, type: typeMap[category] })),
)
})
return allReports
.sort((a, b) => b.time.localeCompare(a.time))
.map(item => ({
id: item.id,
title: item.data.title || item.data.content.match(/^【([^】]*)】(.*)$/)?.[1] || item.data.content,
pubDate: item.time,
extra: { info: item.type, hover: item.data.content },
url: `https://mktnews.net/flashDetail.html?id=${item.id}`,
}))
})
export default defineSource({
"mktnews": flash,
"mktnews-flash": flash,
})
+45 -17
View File
@@ -1,28 +1,56 @@
import * as cheerio from "cheerio"
import process from "node:process"
import type { NewsItem } from "@shared/types"
export default defineSource(async () => {
const baseURL = "https://www.producthunt.com"
const html: any = await myFetch(baseURL)
const $ = cheerio.load(html)
const $main = $("[data-test=homepage-section-0] [data-test^=post-item]")
const apiToken = process.env.PRODUCTHUNT_API_TOKEN
const token = `Bearer ${apiToken}`
if (!apiToken) {
throw new Error("PRODUCTHUNT_API_TOKEN is not set")
}
const query = `
query {
posts(first: 30, order: VOTES) {
edges {
node {
id
name
tagline
votesCount
url
slug
}
}
}
}
`
const response: any = await myFetch("https://api.producthunt.com/v2/api/graphql", {
method: "POST",
headers: {
"Authorization": token,
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({ query }),
})
const news: NewsItem[] = []
$main.each((_, el) => {
const a = $(el).find("a").first()
const url = a.attr("href")
const title = $(el).find("a[data-test^=post-name]").text().replace(/^\d+\.\s*/, "")
const id = $(el).attr("data-test")?.replace("post-item-", "")
const vote = $(el).find("[data-test=vote-button]").text()
if (url && id && title) {
const posts = response?.data?.posts?.edges || []
for (const edge of posts) {
const post = edge.node
if (post.id && post.name) {
news.push({
url: `${baseURL}${url}`,
title,
id,
id: post.id,
title: post.name,
url: post.url || `https://www.producthunt.com/posts/${post.slug}`,
extra: {
info: `△︎ ${vote}`,
info: ` △︎ ${post.votesCount || 0}`,
hover: post.tagline,
},
})
}
})
}
return news
})
+32
View File
@@ -0,0 +1,32 @@
import * as cheerio from "cheerio"
import type { NewsItem } from "@shared/types"
export default defineSource(async () => {
const response: any = await myFetch("https://store.steampowered.com/stats/stats/")
const $ = cheerio.load(response)
const $rows = $("#detailStats tr.player_count_row")
const news: NewsItem[] = []
$rows.each((_, el) => {
const $el = $(el)
const $a = $el.find("a.gameLink")
const url = $a.attr("href")
const gameName = $a.text().trim()
const currentPlayers = $el.find("td:first-child .currentServers").text().trim()
if (url && gameName && currentPlayers) {
const title = gameName
news.push({
url,
title,
id: url,
pubDate: Date.now(),
extra: {
info: currentPlayers,
},
})
}
})
return news
})
+63
View File
@@ -0,0 +1,63 @@
import { myFetch } from "#/utils/fetch"
import { defineSource } from "#/utils/source"
interface WapRes {
ret: number
msg: string
data: {
id: number
name: string
lead: string
cover?: string
shareTitle: string
shareAbstract: string
sharePic: string
is724: boolean
is724Paper: boolean
head_cmsid: string
feed_style: number
head_article: {
live_info: string
title: string
img: string
pub_time: string
media_name: string
}
paperInfo: any
tabs: {
id: string
channel_id: string
name: string
source: string
type: string
articleList: any[]
article_count: number
sub_tab: string
}[]
banner: string
}
}
/**
* 综合早报
*/
const comprehensiveNews = defineSource(async () => {
const url = "https://i.news.qq.com/web_backend/v2/getTagInfo?tagId=aEWqxLtdgmQ%3D"
const res = await myFetch<WapRes>(url, {
headers: {
Referer: "https://news.qq.com/",
},
})
return res.data.tabs[0].articleList.map(news => ({
id: news.id,
title: news.title,
url: news.link_info.url,
extra: {
hover: news.desc,
},
}))
})
export default defineSource({
"tencent-hot": comprehensiveNews,
})
+1 -1
View File
@@ -52,7 +52,7 @@ const news = defineSource(async () => {
const res: NewsRes = await myFetch(apiUrl)
return res.data.items
.filter(k => k.resource_type !== "ad" && k.resource.type !== "live" && k.resource.uri)
.filter(k => k.resource_type !== "theme" && k.resource_type !== "ad" && k.resource.type !== "live" && k.resource.uri)
.map(({ resource: h }) => {
return {
id: h.id,
+47 -43
View File
@@ -1,48 +1,52 @@
interface Res {
ok: number // 1 is ok
data: {
realtime:
{
num: number // 看上去是个 id
emoticon: string
icon?: string // 热,新 icon url
icon_width: number
icon_height: number
is_ad?: number // 1
note: string
small_icon_desc: string
icon_desc?: string // 如果是 荐 ,就是广告
topic_flag: number
icon_desc_color: string
flag: number
word_scheme: string
small_icon_desc_color: string
realpos: number
label_name: string
word: string // 热搜词
rank: number
}[]
}
}
import * as cheerio from "cheerio"
export default defineSource(async () => {
const url = "https://weibo.com/ajax/side/hotSearch"
const res: Res = await myFetch(url)
return res.data.realtime
.filter(k => !k.is_ad)
.map((k) => {
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
return {
id: k.word,
title: k.word,
extra: {
icon: k.icon && {
url: proxyPicture(k.icon),
scale: 1.5,
const baseurl = "https://s.weibo.com"
const url = `${baseurl}/top/summary?cate=realtimehot`
const html = await myFetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
// https://github.com/v5tech/weibo-trending-hot-search
"Cookie": "SUB=_2AkMWIuNSf8NxqwJRmP8dy2rhaoV2ygrEieKgfhKJJRMxHRl-yT9jqk86tRB6PaLNvQZR6zYUcYVT1zSjoSreQHidcUq7",
"referer": url,
},
})
const $ = cheerio.load(html)
const rows = $("#pl_top_realtimehot table tbody tr").slice(1)
const hotNews: NewsItem[] = []
rows.each((_, row) => {
const $row = $(row)
const $link = $row.find("td.td-02 a").filter((_, el) => {
const href = $(el).attr("href")
return !!(href && !href.includes("javascript:void(0);"))
}).first()
if ($link.length) {
const title = $link.text().trim()
const href = $link.attr("href")
if (title && href) {
const $flag = $row.find("td.td-03").text().trim()
const flagUrl = {
: "https://simg.s.weibo.com/moter/flags/1_0.png",
: "https://simg.s.weibo.com/moter/flags/2_0.png",
}[$flag]
hotNews.push({
id: title,
title,
url: `${baseurl}${href}`,
mobileUrl: `${baseurl}${href}`,
extra: {
icon: flagUrl ? { url: proxyPicture(flagUrl), scale: 1.5 } : undefined,
},
},
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`,
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`,
})
}
})
}
})
return hotNews
})
+34 -19
View File
@@ -1,38 +1,53 @@
interface Res {
data: {
card_label?: {
icon: string
night_icon: string
type: "hot_list_feed"
style_type: "1"
feed_specific: {
answer_count: 411
}
target: {
id: number
title: string
url: string
created: number
answer_count: number
follower_count: number
bound_topic_ids: number[]
comment_count: number
is_following: boolean
excerpt: string
title_area: {
text: string
}
excerpt_area: {
text: string
}
image_area: {
url: string
}
metrics_area: {
text: string
font_color: string
background: string
weight: string
}
label_area: {
type: "trend"
trend: number
night_color: string
normal_color: string
}
link: {
url: string
}
}
}[]
}
export default defineSource({
zhihu: async () => {
const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=20&desktop=true"
const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-list-web?limit=20&desktop=true"
const res: Res = await myFetch(url)
return res.data
.map((k) => {
const urlId = k.target.url?.match(/(\d+)$/)?.[1]
return {
id: k.target.id,
title: k.target.title,
id: k.target.link.url.match(/(\d+)$/)?.[1] ?? k.target.link.url,
title: k.target.title_area.text,
extra: {
icon: k.card_label?.night_icon && proxyPicture(k.card_label.night_icon),
info: k.target.metrics_area.text,
hover: k.target.excerpt_area.text,
},
url: `https://www.zhihu.com/question/${urlId || k.target.id}`,
url: k.target.link.url,
}
})
},
+14 -10
View File
@@ -4,9 +4,10 @@
"weibo": "weibo-shishiresou",
"zaobao": "lianhezaobao",
"coolapk": "kuan-jinrizuire",
"wallstreetcn-quick": "huaerjiejianwen-shishikuaixun",
"wallstreetcn-news": "huaerjiejianwen-zuixinzixun",
"wallstreetcn-hot": "huaerjiejianwen-zuirewenzhang",
"mktnews-flash": "MKTNews-kuaixun",
"wallstreetcn-quick": "huaerjiejianwen-kuaixun",
"wallstreetcn-news": "huaerjiejianwen-zuixin",
"wallstreetcn-hot": "huaerjiejianwen-zuire",
"36kr-quick": "36ke-kuaixun",
"douyin": "douyin",
"hupu": "hupu-zhugandaoretie",
@@ -16,8 +17,7 @@
"thepaper": "pengpaixinwen-rebang",
"sputniknewscn": "weixingtongxunshe",
"cankaoxiaoxi": "cankaoxiaoxi",
"pcbeta-windows11": "yuanjingluntan-Windows 11",
"pcbeta-windows": "yuanjingluntan-Windows ziyuan",
"pcbeta-windows11": "yuanjingluntan-Win11",
"cls-telegraph": "cailianshe-dianbao",
"cls-depth": "cailianshe-shendu",
"cls-hot": "cailianshe-remen",
@@ -36,14 +36,18 @@
"kaopu": "kaopuxinwen",
"jin10": "jinshishuju",
"baidu": "baiduresou",
"linuxdo-latest": "LINUX DO-zuixin",
"linuxdo-latest": "LINUX DO-zuixin",
"linuxdo-hot": "LINUX DO-jinrizuire",
"ghxi": "guoheboke",
"smzdm": "shenmezhidemai",
"idcflare-latest": "IDC Flare-zuixin",
"idcflare-hot": "IDC Flare-jinrizuire",
"nowcoder": "niuke",
"sspai": "shaoshupai",
"juejin": "xitujuejin",
"ifeng": "fenghuangwang-redianzixun",
"chongbuluo-latest": "chongbuluo-zuixin",
"chongbuluo-hot": "chongbuluo-zuire"
}
"chongbuluo-hot": "chongbuluo-zuire",
"douban": "douban-remendianying",
"steam": "Steam-zaixianrenshu",
"tencent-hot": "tengxunxinwen-zonghezaobao"
}
+95 -24
View File
@@ -57,6 +57,18 @@ export const originSources = {
title: "今日最热",
home: "https://coolapk.com",
},
"mktnews": {
name: "MKTNews",
column: "finance",
home: "https://mktnews.net",
color: "indigo",
interval: Time.Realtime,
sub: {
flash: {
title: "快讯",
},
},
},
"wallstreetcn": {
name: "华尔街见闻",
color: "blue",
@@ -66,14 +78,14 @@ export const originSources = {
quick: {
type: "realtime",
interval: Time.Fast,
title: "实时快讯",
title: "快讯",
},
news: {
title: "最新资讯",
title: "最新",
interval: Time.Common,
},
hot: {
title: "最热文章",
title: "最热",
type: "hottest",
interval: Time.Common,
},
@@ -159,7 +171,7 @@ export const originSources = {
home: "https://bbs.pcbeta.com",
sub: {
windows11: {
title: "Windows 11",
title: "Win11",
type: "realtime",
interval: Time.Fast,
},
@@ -167,6 +179,7 @@ export const originSources = {
title: "Windows 资源",
type: "realtime",
interval: Time.Fast,
disable: true,
},
},
},
@@ -323,7 +336,6 @@ export const originSources = {
column: "tech",
color: "slate",
home: "https://linux.do/",
disable: "cf",
sub: {
latest: {
title: "最新",
@@ -336,12 +348,31 @@ export const originSources = {
home: "https://linux.do/hot",
},
},
},
"idcflare": {
name: "IDC Flare",
column: "tech",
color: "slate",
home: "https://idcflare.com/",
sub: {
latest: {
title: "最新",
home: "https://idcflare.com/latest",
},
hot: {
title: "今日最热",
type: "hottest",
interval: Time.Common,
home: "https://idcflare.com/hot",
},
},
},
"ghxi": {
name: "果核剥壳",
column: "china",
color: "yellow",
home: "https://www.ghxi.com/",
disable: true,
},
"smzdm": {
name: "什么值得买",
@@ -349,6 +380,7 @@ export const originSources = {
color: "red",
type: "hottest",
home: "https://www.smzdm.com",
disable: true,
},
"nowcoder": {
name: "牛客",
@@ -398,6 +430,36 @@ export const originSources = {
},
},
},
"douban": {
name: "豆瓣",
column: "china",
title: "热门电影",
color: "green",
type: "hottest",
home: "https://www.douban.com",
},
"steam": {
name: "Steam",
column: "world",
title: "在线人数",
color: "blue",
type: "hottest",
home: "https://store.steampowered.com",
},
"tencent": {
name: "腾讯新闻",
column: "china",
color: "blue",
home: "https://news.qq.com",
sub: {
hot: {
title: "综合早报",
type: "hottest",
interval: Time.Common,
home: "https://news.qq.com/tag/aEWqxLtdgmQ=",
},
},
},
} as const satisfies Record<string, OriginSource>
export function genSources() {
@@ -417,29 +479,38 @@ export function genSources() {
if (source.sub && Object.keys(source.sub).length) {
Object.entries(source.sub).forEach(([subId, subSource], i) => {
if (i === 0) {
_.push([id, {
redirect: `${id}-${subId}`,
...parent,
...subSource,
}] as [any, Source])
_.push([
id,
{
redirect: `${id}-${subId}`,
...parent,
...subSource,
},
] as [any, Source])
}
_.push([`${id}-${subId}`, { ...parent, ...subSource }] as [any, Source])
_.push([`${id}-${subId}`, { ...parent, ...subSource }] as [
any,
Source,
])
})
} else {
_.push([id, {
title: source.title,
...parent,
}])
_.push([
id,
{
title: source.title,
...parent,
},
])
}
})
return typeSafeObjectFromEntries(_.filter(([_, v]) => {
if (v.disable === "cf" && process.env.CF_PAGES) {
return false
} else if (v.disable === true) {
return false
} else {
return true
}
}))
return typeSafeObjectFromEntries(
_.filter(([_, v]) => {
if (v.disable === "cf" && process.env.CF_PAGES) {
return false
} else {
return v.disable !== true
}
}),
)
}
+71 -52
View File
@@ -51,6 +51,23 @@
"color": "green",
"interval": 600000
},
"mktnews": {
"redirect": "mktnews-flash",
"name": "MKTNews",
"column": "finance",
"home": "https://mktnews.net",
"color": "indigo",
"interval": 120000,
"title": "快讯"
},
"mktnews-flash": {
"name": "MKTNews",
"column": "finance",
"home": "https://mktnews.net",
"color": "indigo",
"interval": 120000,
"title": "快讯"
},
"wallstreetcn": {
"redirect": "wallstreetcn-quick",
"name": "华尔街见闻",
@@ -59,7 +76,7 @@
"home": "https://wallstreetcn.com/",
"color": "blue",
"interval": 300000,
"title": "实时快讯"
"title": "快讯"
},
"wallstreetcn-quick": {
"name": "华尔街见闻",
@@ -68,7 +85,7 @@
"home": "https://wallstreetcn.com/",
"color": "blue",
"interval": 300000,
"title": "实时快讯"
"title": "快讯"
},
"wallstreetcn-news": {
"name": "华尔街见闻",
@@ -76,7 +93,7 @@
"home": "https://wallstreetcn.com/",
"color": "blue",
"interval": 1800000,
"title": "最新资讯"
"title": "最新"
},
"wallstreetcn-hot": {
"name": "华尔街见闻",
@@ -85,7 +102,7 @@
"home": "https://wallstreetcn.com/",
"color": "blue",
"interval": 1800000,
"title": "最热文章"
"title": "最热"
},
"36kr": {
"redirect": "36kr-quick",
@@ -181,7 +198,7 @@
"home": "https://bbs.pcbeta.com",
"color": "blue",
"interval": 300000,
"title": "Windows 11"
"title": "Win11"
},
"pcbeta-windows11": {
"name": "远景论坛",
@@ -190,16 +207,7 @@
"home": "https://bbs.pcbeta.com",
"color": "blue",
"interval": 300000,
"title": "Windows 11"
},
"pcbeta-windows": {
"name": "远景论坛",
"type": "realtime",
"column": "tech",
"home": "https://bbs.pcbeta.com",
"color": "blue",
"interval": 300000,
"title": "Windows 资源"
"title": "Win11"
},
"cls": {
"redirect": "cls-telegraph",
@@ -406,49 +414,23 @@
"color": "blue",
"interval": 600000
},
"linuxdo": {
"redirect": "linuxdo-latest",
"name": "LINUX DO",
"disable": "cf",
"column": "tech",
"home": "https://linux.do/latest",
"color": "slate",
"interval": 600000,
"title": "最新"
},
"linuxdo-latest": {
"name": "LINUX DO",
"disable": "cf",
"column": "tech",
"home": "https://linux.do/latest",
"color": "slate",
"interval": 600000,
"title": "最新"
},
"linuxdo-hot": {
"name": "LINUX DO",
"type": "hottest",
"disable": "cf",
"column": "tech",
"home": "https://linux.do/hot",
"color": "slate",
"interval": 1800000,
"home": "https://linux.do",
"color": "gray",
"interval": 600000,
"title": "今日最热"
},
"ghxi": {
"name": "果核剥壳",
"column": "china",
"home": "https://www.ghxi.com/",
"color": "yellow",
"interval": 600000
},
"smzdm": {
"name": "什么值得买",
"idcflare-hot": {
"name": "IDC Flare",
"type": "hottest",
"column": "china",
"home": "https://www.smzdm.com",
"color": "red",
"interval": 600000
"column": "tech",
"home": "https://idcflare.com",
"color": "gray",
"interval": 600000,
"title": "今日最热"
},
"nowcoder": {
"name": "牛客",
@@ -508,5 +490,42 @@
"color": "green",
"interval": 1800000,
"title": "最热"
},
"douban": {
"title": "热门电影",
"name": "豆瓣",
"type": "hottest",
"column": "china",
"home": "https://www.douban.com",
"color": "green",
"interval": 600000
},
"steam": {
"title": "在线人数",
"name": "Steam",
"type": "hottest",
"column": "world",
"home": "https://store.steampowered.com",
"color": "blue",
"interval": 600000
},
"tencent": {
"redirect": "tencent-hot",
"name": "腾讯新闻",
"type": "hottest",
"column": "china",
"home": "https://news.qq.com/tag/aEWqxLtdgmQ=",
"color": "blue",
"interval": 1800000,
"title": "综合早报"
},
"tencent-hot": {
"name": "腾讯新闻",
"type": "hottest",
"column": "china",
"home": "https://news.qq.com/tag/aEWqxLtdgmQ=",
"color": "blue",
"interval": 1800000,
"title": "综合早报"
}
}
}
+65 -45
View File
@@ -8,8 +8,10 @@ import { useThrottleFn } from "ahooks"
import { useAutoAnimate } from "@formkit/auto-animate/react"
import { motion } from "framer-motion"
import { useWindowSize } from "react-use"
import { isMobile } from "react-device-detect"
import { DndContext } from "../common/dnd"
import { useSortable } from "../common/dnd/useSortable"
import { OverlayScrollbar } from "../common/overlay-scrollbar"
import type { ItemsProps } from "./card"
import { CardWrapper } from "./card"
import { currentSourcesAtom } from "~/atoms"
@@ -26,58 +28,76 @@ export function Dnd() {
return Math.min(width - 32, WIDTH)
}, [width])
if (!items.length) return null
return (
<DndWrapper items={items} setItems={setItems}>
<motion.ol
className="grid w-full gap-6"
ref={parent}
style={{
gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}px, 1fr))`,
}}
initial="hidden"
animate="visible"
variants={{
hidden: {
opacity: 0,
},
visible: {
opacity: 1,
transition: {
delayChildren: 0.1,
staggerChildren: 0.1,
<DndWrapper items={items} setItems={setItems} isSingleColumn={isMobile}>
<OverlayScrollbar defer className="overflow-x-auto">
<motion.ol
className={isMobile
? "flex px-2 gap-6 pb-4 scroll-smooth"
: "grid w-full gap-6"}
ref={parent}
style={isMobile
? {
// 横向滚动布局
}
: {
gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}px, 1fr))`,
}}
initial="hidden"
animate="visible"
variants={{
hidden: {
opacity: 0,
},
},
}}
>
{items.map(id => (
<motion.li
key={id}
transition={{
type: "tween",
duration: AnimationDuration / 1000,
}}
variants={{
hidden: {
y: 20,
opacity: 0,
visible: {
opacity: 1,
transition: {
delayChildren: 0.1,
staggerChildren: 0.1,
},
visible: {
y: 0,
opacity: 1,
},
}}
>
<SortableCardWrapper id={id} />
</motion.li>
))}
</motion.ol>
},
}}
>
{items.map((id, index) => (
<motion.li
key={id}
className={$(isMobile && "flex-shrink-0", isMobile && index === items.length - 1 && "mr-2")}
style={isMobile ? { width: `${width - 16 > WIDTH ? WIDTH : width - 16}px` } : undefined}
transition={{
type: "tween",
duration: AnimationDuration / 1000,
}}
variants={{
hidden: {
y: 20,
opacity: 0,
},
visible: {
y: 0,
opacity: 1,
},
}}
>
<SortableCardWrapper id={id} />
</motion.li>
))}
</motion.ol>
</OverlayScrollbar>
{isMobile && (
<div className="flex justify-center">
<span className="text-sm text-gray-500 text-center"></span>
</div>
)}
</DndWrapper>
)
}
function DndWrapper({ items, setItems, children }: PropsWithChildren<{
function DndWrapper({ items, setItems, isSingleColumn, children }: PropsWithChildren<{
items: SourceID[]
setItems: (items: SourceID[]) => void
isSingleColumn: boolean
}>) {
const onDropTargetChange = useCallback(({ location, source }: BaseEventPayload<ElementDragType>) => {
const traget = location.current.dropTargets[0]
@@ -91,10 +111,10 @@ function DndWrapper({ items, setItems, children }: PropsWithChildren<{
startIndex: fromIndex,
indexOfTarget: toIndex,
closestEdgeOfTarget,
axis: "vertical",
axis: isSingleColumn ? "horizontal" : "vertical",
})
setItems(update)
}, [items, setItems])
}, [items, setItems, isSingleColumn])
// 避免动画干扰
const { run } = useThrottleFn(onDropTargetChange, {
leading: true,
+1 -1
View File
@@ -19,7 +19,7 @@
}
[cmdk-list] {
--at-apply: px-3 flex flex-col gap-2 items-stretch h-400px;
--at-apply: px-3 flex flex-col gap-2 items-stretch max-h-[400px] h-[calc(100vh-100px)];
}
[cmdk-group-heading] {
+7
View File
@@ -17,6 +17,12 @@ function GoTop() {
)
}
function Github() {
return (
<button type="button" title="Github" className="i-ph:github-logo-duotone btn" onClick={() => window.open(Homepage)} />
)
}
function Refresh() {
const currentSources = useAtomValue(currentSourcesAtom)
const { refresh } = useRefetch()
@@ -65,6 +71,7 @@ export function Header() {
<span className="justify-self-end flex gap-2 items-center text-xl text-primary-600 dark:text-primary">
<GoTop />
<Refresh />
<Github />
<Menu />
</span>
</>
+14 -14
View File
@@ -1,16 +1,16 @@
import { motion } from "framer-motion"
function ThemeToggle() {
const { isDark, toggleDark } = useDark()
return (
<li onClick={toggleDark} className="cursor-pointer [&_*]:cursor-pointer transition-all">
<span className={$("inline-block", isDark ? "i-ph-moon-stars-duotone" : "i-ph-sun-dim-duotone")} />
<span>
{isDark ? "浅色模式" : "深色模式"}
</span>
</li>
)
}
// function ThemeToggle() {
// const { isDark, toggleDark } = useDark()
// return (
// <li onClick={toggleDark} className="cursor-pointer [&_*]:cursor-pointer transition-all">
// <span className={$("inline-block", isDark ? "i-ph-moon-stars-duotone" : "i-ph-sun-dim-duotone")} />
// <span>
// {isDark ? "浅色模式" : "深色模式"}
// </span>
// </li>
// )
// }
export function Menu() {
const { loggedIn, login, logout, userInfo, enableLogin } = useLogin()
@@ -64,7 +64,7 @@ export function Menu() {
<span>Github </span>
</li>
))}
<ThemeToggle />
{/* <ThemeToggle /> */}
<li onClick={() => window.open(Homepage)} className="cursor-pointer [&_*]:cursor-pointer transition-all">
<span className="i-ph:github-logo-duotone inline-block" />
<span>Star on Github </span>
@@ -75,7 +75,7 @@ export function Menu() {
>
<img
alt="GitHub stars badge"
src="https://img.shields.io/github/stars/ourongxing/newsnow?logo=github"
src="https://img.shields.io/github/stars/ourongxing/newsnow?logo=github&style=flat&labelColor=%235e3c40&color=%23614447"
/>
</a>
<a
@@ -83,7 +83,7 @@ export function Menu() {
>
<img
alt="GitHub forks badge"
src="https://img.shields.io/github/forks/ourongxing/newsnow?logo=github"
src="https://img.shields.io/github/forks/ourongxing/newsnow?logo=github&style=flat&labelColor=%235e3c40&color=%23614447"
/>
</a>
</li>
+8 -5
View File
@@ -4,6 +4,7 @@ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import type { QueryClient } from "@tanstack/react-query"
import { isMobile } from "react-device-detect"
import { Header } from "~/components/header"
import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar"
import { Footer } from "~/components/footer"
@@ -30,11 +31,13 @@ function RootComponent() {
usePWA()
return (
<>
<GlobalOverlayScrollbar className={$([
"h-full overflow-x-auto px-4",
"md:(px-10)",
"lg:(px-24)",
])}
<GlobalOverlayScrollbar
className={$([
!isMobile && "px-4",
"h-full overflow-x-auto",
"md:(px-10)",
"lg:(px-24)",
])}
>
<header
className={$([
+1
View File
@@ -8,6 +8,7 @@
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"strict": true,
"allowJs": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,