Skip to content

Commit

Permalink
Merge pull request #158 from besscroft/v2
Browse files Browse the repository at this point in the history
v2.0.2
  • Loading branch information
besscroft authored Nov 15, 2024
2 parents 63cda51 + 242e603 commit b319899
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 50 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ PicImpact 是一个摄影师专用的摄影作品展示网站,基于 Next.js +
- 响应式设计,在 PC 和移动端都有不错的体验,支持暗黑模式。
- 图片存储兼容 S3 API、Cloudflare R2、AList API。
- 图片支持绑定标签,并且可通过标签进行交互,筛选标签下所有图片。
- 支持输出 RSS,可以使用 [Follow](https://github.com/RSSNext/Follow) 订阅,并支持订阅源所有权验证。
- 支持批量自动化上传,上传图片时会生成 0.3 倍率的压缩图片,以提供加载优化。
- 图片版权信息展示和维护功能,支持外链跳转。
- 后台有图片数据统计、图片上传、图片维护、相册管理、系统设置和存储配置功能。
Expand All @@ -34,7 +35,7 @@ PicImpact 是一个摄影师专用的摄影作品展示网站,基于 Next.js +
### TODO

- [ ] 单独的存储管理功能(不会影响现有功能,属于扩展)。
- [ ] RSS 支持,能够使用 Follow 订阅。
- [x] RSS 支持,能够使用 Follow 订阅。
- [ ] Web Analytics 支持。
- [ ] OneDrive 支持。

Expand Down
1 change: 0 additions & 1 deletion app/admin/settings/password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export default function PassWord() {
})

async function updatePassword(data: z.infer<typeof FormSchema>) {
console.log(data)
if (data.onePassword === '') {
toast.error('请输入旧密码!')
return
Expand Down
90 changes: 82 additions & 8 deletions app/admin/settings/preferences/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@ import React, { useEffect, useState } from 'react'
import useSWR from 'swr'
import { fetcher } from '~/lib/utils/fetcher'
import { toast } from 'sonner'
import { Input } from '~/components/ui/input'
import { ReloadIcon } from '@radix-ui/react-icons'
import { Button } from '~/components/ui/button'
import { Label } from '~/components/ui/label'

export default function Preferences() {
const [title, setTitle] = useState('')
const [customFaviconUrl, setCustomFaviconUrl] = useState('')
const [customAuthor, setCustomAuthor] = useState('')
const [feedId, setFeedId] = useState('')
const [userId, setUserId] = useState('')
const [loading, setLoading] = useState(false)

const { data, isValidating, isLoading } = useSWR('/api/v1/settings/get-custom-title', fetcher)
const { data, isValidating, isLoading } = useSWR('/api/v1/settings/get-custom-info', fetcher)

async function updateTitle() {
async function updateInfo() {
try {
setLoading(true)
await fetch('/api/v1/settings/update-custom-title', {
await fetch('/api/v1/settings/update-custom-info', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: title,
customFaviconUrl: customFaviconUrl,
customAuthor: customAuthor,
feedId: feedId,
userId: userId,
}),
}).then(res => res.json())
toast.success('修改成功!')
Expand All @@ -36,7 +42,11 @@ export default function Preferences() {
}

useEffect(() => {
setTitle(data?.config_value || '')
setTitle(data?.find((item: any) => item.config_key === 'custom_title')?.config_value || '')
setCustomFaviconUrl(data?.find((item: any) => item.config_key === 'custom_favicon_url')?.config_value || '')
setCustomAuthor(data?.find((item: any) => item.config_key === 'custom_author')?.config_value || '')
setFeedId(data?.find((item: any) => item.config_key === 'rss_feed_id')?.config_value || '')
setUserId(data?.find((item: any) => item.config_key === 'rss_user_id')?.config_value || '')
}, [data])

return (
Expand All @@ -57,11 +67,75 @@ export default function Preferences() {
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
<label
htmlFor="customFaviconUrl"
className="w-full sm:w-64 block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-blue-600 focus-within:ring-1 focus-within:ring-blue-600"
>
<span className="text-xs font-medium text-gray-700"> favicon </span>

<input
type="text"
id="customFaviconUrl"
disabled={isValidating || isLoading}
value={customFaviconUrl || ''}
placeholder="请输入 favicon 地址"
onChange={(e) => setCustomFaviconUrl(e.target.value)}
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
<label
htmlFor="customAuthor"
className="w-full sm:w-64 block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-blue-600 focus-within:ring-1 focus-within:ring-blue-600"
>
<span className="text-xs font-medium text-gray-700"> 网站归属者名称 </span>

<input
type="text"
id="customAuthor"
disabled={isValidating || isLoading}
value={customAuthor || ''}
placeholder="请输入网站归属者名称。"
onChange={(e) => setCustomAuthor(e.target.value)}
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
<label
htmlFor="feedId"
className="w-full sm:w-64 block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-blue-600 focus-within:ring-1 focus-within:ring-blue-600"
>
<span className="text-xs font-medium text-gray-700"> RSS feedId </span>

<input
type="text"
id="feedId"
disabled={isValidating || isLoading}
value={feedId || ''}
placeholder="请输入 RSS feedId"
onChange={(e) => setFeedId(e.target.value)}
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
<label
htmlFor="userId"
className="w-full sm:w-64 block overflow-hidden rounded-md border border-gray-200 px-3 py-2 shadow-sm focus-within:border-blue-600 focus-within:ring-1 focus-within:ring-blue-600"
>
<span className="text-xs font-medium text-gray-700"> RSS userId </span>

<input
type="text"
id="userId"
disabled={isValidating || isLoading}
value={userId || ''}
placeholder="请输入 RSS userId"
onChange={(e) => setUserId(e.target.value)}
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
<div className="flex w-full sm:w-64 items-center justify-center space-x-1">
<Button
variant="outline"
disabled={loading}
onClick={() => updateTitle()}
disabled={loading || isValidating}
onClick={() => updateInfo()}
aria-label="提交"
>
{loading && <ReloadIcon className="mr-2 h-4 w-4 animate-spin"/>}
Expand Down
8 changes: 4 additions & 4 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ProgressBarProviders } from '~/app/providers/progress-bar-providers'
import { ButtonStoreProvider } from '~/app/providers/button-store-Providers'

import '~/style/globals.css'
import { fetchCustomTitle } from '~/server/db/query'
import { fetchCustomInfo } from '~/server/db/query'

type Props = {
params: { id: string }
Expand All @@ -19,11 +19,11 @@ export async function generateMetadata(
parent: ResolvingMetadata
): Promise<Metadata> {

const data = await fetchCustomTitle()
const data = await fetchCustomInfo()

return {
title: data?.config_value || 'PicImpact',
icons: { icon: './favicon.ico' },
title: data?.find((item: any) => item.config_key === 'custom_title')?.config_value || 'PicImpact',
icons: { icon: data?.find((item: any) => item.config_key === 'custom_favicon_url')?.config_value || './favicon.ico' },
}
}

Expand Down
74 changes: 74 additions & 0 deletions app/rss.xml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'server-only'
import RSS from 'rss'
import { fetchCustomInfo, getRSSImages } from '~/server/db/query'

export async function GET(request: Request) {
const data = await fetchCustomInfo()

const url = new URL(request.url);

const feedId = data?.find((item: any) => item.config_key === 'rss_feed_id')?.config_value?.toString();
const userId = data?.find((item: any) => item.config_key === 'rss_user_id')?.config_value?.toString();

const customElements = feedId && userId
? [
{
follow_challenge: [
{ feedId: feedId },
{ userId: userId }
]
}
]
: [];

const feed = new RSS({
title: data?.find((item: any) => item.config_key === 'custom_title')?.config_value?.toString() || '相册',
generator: 'RSS for Next.js',
feed_url: `${url.origin}/rss.xml`,
site_url: url.origin,
copyright: `© 2024${new Date().getFullYear().toString() === '2024' ? '' : `-${new Date().getFullYear().toString()}`} ${
data?.find((item: any) => item.config_key === 'custom_author')?.config_value?.toString() || ''
}.`,
pubDate: new Date().toUTCString(),
ttl: 60,
custom_elements: customElements,
});

const images = await getRSSImages()
if (Array.isArray(images) && images.length > 0) {
images?.map(item => {
feed.item({
title: item.title || '图片',
description: `
<div>
<img src="${item.preview_url || item.url}" alt="${item.detail}" />
<p>${item.detail}</p>
<a href="${url.origin + (item.album_value === '/' ? '/preview/' : item.album_value + '/preview/') + item.id}" target="_blank">查看图片信息</a>
</div>
`,
url: url.origin + (item.album_value === '/' ? '/preview/' : item.album_value + '/preview/') + item.id,
guid: item.id,
date: item.created_at,
enclosure: {
url: item.preview_url || item.url,
type: 'image/jpeg',
},
media: {
content: {
url: item.preview_url || item.url,
type: 'image/jpeg',
},
thumbnail: {
url: item.preview_url || item.url,
},
},
})
})
}

return new Response(feed.xml(), {
headers: {
'Content-Type': 'application/xml',
}
});
}
19 changes: 9 additions & 10 deletions components/admin/list/ListProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,14 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
<AlertDialogCancel onClick={() => {
setImage({} as ImageType)
setImageAlbum('')
}}>Cancel</AlertDialogCancel>
<AlertDialogAction>
<Button
disabled={updateImageAlbumLoading}
onClick={() => updateImageAlbum()}
aria-label="更新"
>
{updateImageAlbumLoading && <ReloadIcon className="mr-2 h-4 w-4 animate-spin"/>}
更新
</Button>
}}>取消</AlertDialogCancel>
<AlertDialogAction
disabled={updateImageAlbumLoading}
onClick={() => updateImageAlbum()}
aria-label="更新"
>
{updateImageAlbumLoading && <ReloadIcon className="mr-2 h-4 w-4 animate-spin"/>}
更新
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
Expand Down Expand Up @@ -330,6 +328,7 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
total={total}
pageSize={8}
hideOnSinglePage
showSizeChanger={false}
onChange={async (page, pageSize) => {
setPageNum(page)
await mutate()
Expand Down
1 change: 0 additions & 1 deletion components/admin/upload/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ export default function FileUpload() {
// @ts-ignore
body: JSON.stringify(data),
}).then(res => res.json())
console.log(res)
if (res?.code === 200) {
toast.success('保存成功!')
} else {
Expand Down
9 changes: 6 additions & 3 deletions components/layout/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import Image from 'next/image'
import favicon from '~/public/favicon.svg'
import Link from 'next/link'
import { fetchCustomInfo } from '~/server/db/query'

export default async function Logo() {
const data = await fetchCustomInfo()

export default function Logo() {
return (
<Link href="/" className="select-none">
<Image
src={favicon}
src={data?.find((item: any) => item.config_key === 'custom_favicon_url')?.config_value || favicon}
alt="Logo"
width={36}
height={36}
/>
</Link>
);
);
}
24 changes: 17 additions & 7 deletions hono/settings.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import 'server-only'
import { fetchCustomTitle, fetchR2Info, fetchS3Info, fetchSecretKey, fetchUserById } from '~/server/db/query'
import { fetchCustomInfo, fetchR2Info, fetchS3Info, fetchSecretKey, fetchUserById } from '~/server/db/query'
import { Config } from '~/types'
import { updateAListConfig, updateCustomTitle, updatePassword, updateR2Config, updateS3Config } from '~/server/db/operate'
import { updateAListConfig, updateCustomInfo, updatePassword, updateR2Config, updateS3Config } from '~/server/db/operate'
import { auth } from '~/server/auth'
import CryptoJS from 'crypto-js'
import { Hono } from 'hono'

const app = new Hono()

app.get('/get-custom-title', async (c) => {
const data = await fetchCustomTitle();
app.get('/get-custom-info', async (c) => {
const data = await fetchCustomInfo();
return c.json(data)
})

Expand Down Expand Up @@ -64,10 +64,20 @@ app.put('/update-s3-info', async (c) => {
return c.json(data)
})

app.put('/update-custom-title', async (c) => {
app.put('/update-custom-info', async (c) => {
const query = await c.req.json()
const data = await updateCustomTitle(query.title);
return c.json(data)
try {
await updateCustomInfo(query.title, query.customFaviconUrl, query.customAuthor, query.feedId, query.userId);
return c.json({
code: 200,
message: '更新成功!'
})
} catch (e) {
return Response.json({
code: 500,
message: '更新失败!'
})
}
})

app.put('/update-password', async (c) => {
Expand Down
4 changes: 4 additions & 0 deletions instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export async function register() {
{ config_key: 'auth_enable', config_value: 'false', detail: '是否启用双因素验证' },
{ config_key: 'auth_temp_secret', config_value: '', detail: '双因素验证临时种子密钥' },
{ config_key: 'auth_secret', config_value: '', detail: '双因素验证种子密钥' },
{ config_key: 'custom_favicon_url', config_value: '', detail: '用户自定义的 favicon 地址' },
{ config_key: 'custom_author', config_value: '', detail: '网站归属者名称' },
{ config_key: 'rss_feed_id', config_value: '', detail: 'Follow feedId' },
{ config_key: 'rss_user_id', config_value: '', detail: 'Follow userId' },
],
skipDuplicates: true,
})
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"react-dom": "19.0.0-rc-5c56b873-20241107",
"react-hook-form": "^7.53.1",
"react-photo-album": "^3.0.2",
"rss": "^1.2.2",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"sonner": "^1.5.0",
Expand All @@ -80,6 +81,7 @@
"@types/node": "^20.17.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/rss": "^0.0.32",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "^15.0.3",
Expand Down
Loading

0 comments on commit b319899

Please sign in to comment.