Skip to content

Commit

Permalink
Merge branch 'besscroft:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
minimua authored Dec 4, 2024
2 parents 74d8e65 + 7003041 commit f87da6e
Show file tree
Hide file tree
Showing 21 changed files with 173 additions and 66 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ dist
/build

# local env files
.env
.env

# nextjs --experimental-https cert file
certificates
41 changes: 17 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,6 @@ PicImpact 是一个摄影师专用的摄影作品展示网站,基于 Next.js +
- 基于 prisma 的自动初始化数据库和数据迁移,简化部署流程。
- 支持 Vercel 部署、Node.js 部署、Docker 等容器化部署,当然 k8s 也支持。

### TODO

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

> 会抽空进行开发与维护,也欢迎 PR!
### 如何部署

你可以点击下面的按钮来一键部署到 Vercel,**然后将 `Build Command` 设置为 `pnpm run build:vercel`**,也可以 Fork 项目后手动部署到任何支持的平台。
Expand Down Expand Up @@ -77,10 +68,24 @@ docker run -d --name picimpact \
### 存储配置

暂时提供了 AWS S3 API、Cloudflare R2、AList API 支持,您在部署成功后,可以去 `设置` -> `存储` 进行管理。
> 暂时提供了 AWS S3 API、Cloudflare R2、AList API 支持,您在部署成功后,可以去 `设置` -> `存储` 进行管理。
>
> 原则上优先支持 Cloudflare R2 和 AWS S3 API。
我比较推荐 Cloudflare R2,算是很良心的了,流量免费。

- Cloudflare R2 配置

| Key | 备注 |
|---------------------|--------------------------------------------------------------------------|
| r2_accesskey_id | Cloudflare AccessKey_ID |
| r2_accesskey_secret | Cloudflare AccessKey_Secret |
| r2_endpoint | Cloudflare Endpoint 地域节点,如:`https://<ACCOUNT_ID>.r2.cloudflarestorage.com` |
| r2_bucket | Cloudflare Bucket 存储桶名称,如:`picimpact` |
| r2_storage_folder | 存储文件夹(Cloudflare R2),严格格式,如:`picimpact``picimpact/images` ,填 `/` 或者不填表示根路径 |
| r2_public_domain | Cloudflare R2 自定义域(公开访问) |


- AWS S3 配置

| Key | 备注 |
Expand All @@ -95,17 +100,6 @@ docker run -d --name picimpact \
| s3_cdn | 是否启用 S3 CDN 模式,路径将返回 cdn 地址,默认 false。 |
| s3_cdn_url | cdn 地址,如:`https://cdn.example.com` |

- Cloudflare R2 配置

| Key | 备注 |
|---------------------|--------------------------------------------------------------------------|
| r2_accesskey_id | Cloudflare AccessKey_ID |
| r2_accesskey_secret | Cloudflare AccessKey_Secret |
| r2_endpoint | Cloudflare Endpoint 地域节点,如:`https://<ACCOUNT_ID>.r2.cloudflarestorage.com` |
| r2_bucket | Cloudflare Bucket 存储桶名称,如:`picimpact` |
| r2_storage_folder | 存储文件夹(Cloudflare R2),严格格式,如:`picimpact``picimpact/images` ,填 `/` 或者不填表示根路径 |
| r2_public_domain | Cloudflare R2 自定义域(公开访问) |

- AList API 配置

| Key | 备注 |
Expand Down Expand Up @@ -133,11 +127,11 @@ pnpm run dev

PicImpact 欢迎各种贡献,包括但不限于改进,新功能,文档和代码改进,问题和错误报告。

`v1` 分支目前仅维护和修复 bug
`v1` 目前停止维护

`v2` 分支开发下一个版本,同时接受 `PR`

> 有需求和建议都可以提,有空的话我会处理,但受限于 Next / SSR 的⌈局限性⌋,很多功能的设计上可能会有取舍。
> 有需求和建议都可以提,有空的话我会处理,但受限于 Next / SSR 的⌈局限性⌋,以及照顾移动端使用体验,很多功能的设计上可能会有取舍。
### 隐私安全

Expand All @@ -162,7 +156,6 @@ PicImpact 欢迎各种贡献,包括但不限于改进,新功能,文档和
- [Next.js](https://github.com/vercel/next.js)
- [Hono.js](https://github.com/honojs/hono)
- UI 框架:
- [Next UI](https://github.com/nextui-org/nextui)
- [Radix](https://www.radix-ui.com/)
- [shadcn/ui](https://ui.shadcn.com/)
- 更多组件参见 package.json
Expand Down
2 changes: 1 addition & 1 deletion app/(default)/[...album]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Masonry from '~/components/Masonry'
import Masonry from '~/components/album/Masonry'
import { fetchClientImagesListByAlbum, fetchClientImagesPageTotalByAlbum } from '~/server/db/query'
import { ImageHandleProps } from '~/types'

Expand Down
2 changes: 1 addition & 1 deletion app/(default)/label/[...tag]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Masonry from '~/components/Masonry'
import Masonry from '~/components/album/Masonry'
import { fetchClientImagesListByTag, fetchClientImagesPageTotalByTag } from '~/server/db/query'
import { ImageHandleProps } from '~/types'

Expand Down
2 changes: 1 addition & 1 deletion app/(default)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Masonry from '~/components/Masonry'
import Masonry from '~/components/album/Masonry'
import { fetchClientImagesListByAlbum, fetchClientImagesPageTotalByAlbum } from '~/server/db/query'
import { ImageHandleProps } from '~/types'

Expand Down
2 changes: 1 addition & 1 deletion app/admin/album/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fetchAlbumsList } from '~/server/db/query'
import AlbumList from '~/components/admin/album/AlbumList'
import RefreshButton from '~/components/RefreshButton'
import RefreshButton from '~/components/album/RefreshButton'
import { HandleProps } from '~/types'
import React from 'react'
import AlbumAddSheet from '~/components/admin/album/AlbumAddSheet'
Expand Down
2 changes: 1 addition & 1 deletion app/admin/copyright/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HandleProps } from '~/types'
import React from 'react'
import CopyrightList from '~/components/admin/copyright/CopyrightList'
import CopyrightAddButton from '~/components/admin/copyright/CopyrightAddButton'
import RefreshButton from '~/components/RefreshButton'
import RefreshButton from '~/components/album/RefreshButton'
import CopyrightAddSheet from '~/components/admin/copyright/CopyrightAddSheet'
import CopyrightEditSheet from '~/components/admin/copyright/CopyrightEditSheet'

Expand Down
29 changes: 29 additions & 0 deletions components/admin/album/AlbumAddSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,35 @@ export default function AlbumAddSheet(props : Readonly<HandleProps>) {
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
<label
htmlFor="detail"
className="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="detail"
value={data?.license}
placeholder="CC BY-NC-SA 4.0"
onChange={(e) => setData({...data, license: 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 flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="flex flex-col gap-1">
<div className="text-medium">允许下载</div>
<div className="text-tiny text-default-400">
是否显示下载/复制按钮。
</div>
</div>
<Switch
checked={data?.allow_download === 0}
onCheckedChange={(value) => {
setData({...data, allow_download: value ? 0 : 1})
}}
/>
</div>
<div className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="flex flex-col gap-1">
<div className="text-medium">显示状态</div>
Expand Down
29 changes: 29 additions & 0 deletions components/admin/album/AlbumEditSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,35 @@ export default function AlbumEditSheet(props : Readonly<HandleProps>) {
className="mt-1 w-full border-none p-0 focus:border-transparent focus:outline-none focus:ring-0 sm:text-sm"
/>
</label>
<label
htmlFor="detail"
className="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="detail"
value={album?.license}
placeholder="CC BY-NC-SA 4.0"
onChange={(e) => setAlbumEditData({...album, license: 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 flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="flex flex-col gap-1">
<div className="text-medium">允许下载</div>
<div className="text-tiny text-default-400">
是否显示下载/复制按钮。
</div>
</div>
<Switch
checked={album?.allow_download === 0}
onCheckedChange={(value) => {
setAlbumEditData({...album, allow_download: value ? 0 : 1})
}}
/>
</div>
<div className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="flex flex-col gap-1">
<div className="text-medium">显示状态</div>
Expand Down
6 changes: 3 additions & 3 deletions components/admin/list/ImageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { DataProps, ImageType } from '~/types'
import React from 'react'
import { fetcher } from '~/lib/utils/fetcher'
import useSWR from 'swr'
import ExifView from '~/components/ExifView'
import ExifView from '~/components/album/ExifView'
import { Switch } from '~/components/ui/switch'
import LivePhoto from '~/components/LivePhoto.tsx'
import MultipleSelector from '~/components/ui/origin/multiselect.tsx'
import LivePhoto from '~/components/album/LivePhoto'
import MultipleSelector from '~/components/ui/origin/multiselect'

export default function ImageView() {
const { imageView, imageViewData, setImageView, setImageViewData } = useButtonStore(
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions components/Masonry.tsx → components/album/Masonry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { ImageHandleProps, ImageType } from '~/types'
import { MasonryPhotoAlbum, RenderImageContext, RenderImageProps } from 'react-photo-album'
import { useSWRPageTotalHook } from '~/hooks/useSWRPageTotalHook'
import useSWRInfinite from 'swr/infinite'
import MasonryItem from '~/components/MasonryItem'
import MasonryItem from '~/components/album/MasonryItem'
import { toast } from 'sonner'
import { useSearchParams } from 'next/navigation'
import BlurImage from '~/components/BlurImage'
import BlurImage from '~/components/album/BlurImage'
import { useButtonStore } from '~/app/providers/button-store-Providers'
import { FloatButton } from 'antd'

Expand Down
94 changes: 64 additions & 30 deletions components/MasonryItem.tsx → components/album/MasonryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import {
Crosshair,
Timer,
CircleGauge,
CircleAlert,
ExternalLink
} from 'lucide-react'
import * as React from 'react'
import { useRouter } from 'next-nprogress-bar'
import ExifView from '~/components/ExifView'
import ExifView from '~/components/album/ExifView'
import { toast } from 'sonner'
import { usePathname } from 'next/navigation'
import useSWR from 'swr'
Expand All @@ -39,7 +40,7 @@ import { DownloadIcon } from '~/components/icons/download'
import { LinkIcon } from '~/components/icons/link'
import { ArrowLeftIcon } from '~/components/icons/arrow-left'
import { ArrowRightIcon } from '~/components/icons/arrow-right'
import LivePhoto from '~/components/LivePhoto.tsx'
import LivePhoto from '~/components/album/LivePhoto'

dayjs.extend(customParseFormat)

Expand All @@ -54,6 +55,7 @@ export default function MasonryItem() {
const props: DataProps = {
data: MasonryViewData,
}
const tabsListRef = React.useRef<HTMLDivElement>(null);

const loadingHandle = React.useCallback(async (handle: string) => {
const idx = MasonryViewDataList.findIndex((item: ImageType) => MasonryViewData.id === item.id)
Expand All @@ -73,7 +75,12 @@ export default function MasonryItem() {
async function downloadImg() {
setDownload(true)
try {
toast.warning('开始下载,原图较大,请耐心等待!', { duration: 1500 })
let msg = '开始下载,原图较大,请耐心等待!'
if (MasonryViewData.album_license != null) {
msg += '图片版权归作者所有, 分享转载需遵循 ' + MasonryViewData.album_license + ' 许可协议!'
}

toast.warning(msg, { duration: 1500 })
await fetch(`/api/open/get-image-blob?imageUrl=${MasonryViewData.url}`)
.then((response) => response.blob())
.then((blob) => {
Expand All @@ -96,6 +103,10 @@ export default function MasonryItem() {

React.useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (tabsListRef.current && tabsListRef.current.contains(e.target as Node)) {
return;
}

if (MasonryView) {
if (e.key === "ArrowLeft") {
loadingHandle("prev");
Expand Down Expand Up @@ -171,7 +182,7 @@ export default function MasonryItem() {
</Button>
</div>
}
<Tabs defaultValue="detail" className="w-full" aria-label="图片预览选择项">
<Tabs defaultValue="detail" className="w-full" ref={tabsListRef} aria-label="图片预览选择项">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="detail">
<div className="flex items-center space-x-2 select-none">
Expand All @@ -195,23 +206,30 @@ export default function MasonryItem() {
<TabsContent value="detail">
<div className="flex flex-col space-y-2">
<div className="flex space-x-2">
<Button
onClick={async () => {
try {
const url = MasonryViewData.url
// @ts-ignore
await navigator.clipboard.writeText(url);
toast.success('复制图片链接成功!', {duration: 500})
} catch (error) {
toast.error('复制图片链接失败!', {duration: 500})
}
}}
variant="outline"
className="active:scale-95 duration-200 ease-in-out"
>
<CopyIcon />
复制
</Button>
{
MasonryViewData.album_allow_download === 0 &&
<Button
onClick={async () => {
try {
const url = MasonryViewData.url
// @ts-ignore
await navigator.clipboard.writeText(url);
let msg = '复制图片链接成功!'
if (MasonryViewData.album_license != null) {
msg = '图片版权归作者所有, 分享转载需遵循 ' + MasonryViewData.album_license + ' 许可协议!'
}
toast.success(msg, {duration: 1500})
} catch (error) {
toast.error('复制图片链接失败!', {duration: 500})
}
}}
variant="outline"
className="active:scale-95 duration-200 ease-in-out"
>
<CopyIcon />
复制
</Button>
}
<Button
onClick={async () => {
try {
Expand All @@ -229,16 +247,32 @@ export default function MasonryItem() {
<LinkIcon />
分享
</Button>
<Button
onClick={() => downloadImg()}
disabled={download}
variant="outline"
className="active:scale-95 duration-200 ease-in-out"
>
{download ? <ReloadIcon className="mr-2 h-4 w-4 animate-spin"/> : <DownloadIcon />}
下载
</Button>
{
MasonryViewData.album_allow_download === 0 &&
<Button
onClick={() => downloadImg()}
disabled={download}
variant="outline"
className="active:scale-95 duration-200 ease-in-out"
>
{download ? <ReloadIcon className="mr-2 h-4 w-4 animate-spin"/> : <DownloadIcon />}
下载
</Button>
}
</div>
{
MasonryViewData.album_allow_download === 0 && (
<Card className="py-4 show-up-motion">
<div className="pb-0 pt-2 px-4 flex-col items-start">
<div className="flex items-center space-x-1">
<CircleAlert size={20}/>
<p className="text-tiny uppercase font-bold select-none">转载提示</p>
</div>
<h4 className="font-bold text-large">本作品由作者版权所有,未经授权禁止转载、下载及使用。</h4>
</div>
</Card>
)
}
{MasonryViewData?.exif?.model && MasonryViewData?.exif?.f_number
&& MasonryViewData?.exif?.exposure_time && MasonryViewData?.exif?.focal_length
&& MasonryViewData?.exif?.iso_speed_rating && MasonryViewData?.exif?.make &&
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions prisma/migrations/20241129025605_v1_1/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "albums" ADD COLUMN "allow_download" SMALLINT NOT NULL DEFAULT 1,
ADD COLUMN "license" TEXT;
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ model Albums {
detail String? @db.Text
show Int @default(1) @db.SmallInt
sort Int @default(0) @db.SmallInt
allow_download Int @default(1) @db.SmallInt
license String? @db.Text
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp()
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp()
del Int @default(0) @db.SmallInt
Expand Down
Loading

0 comments on commit f87da6e

Please sign in to comment.