diff --git a/frontend/.gitignore b/frontend/.gitignore index 9ee0d26..6f9c7cf 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -48,4 +48,5 @@ next-env.d.ts create_app.json .open-next -.wrangler/ \ No newline at end of file +.wrangler/ +.dev.vars \ No newline at end of file diff --git a/frontend/open-next.config.ts b/frontend/open-next.config.ts index 100253a..59b156c 100644 --- a/frontend/open-next.config.ts +++ b/frontend/open-next.config.ts @@ -1,6 +1,4 @@ import { defineCloudflareConfig } from '@opennextjs/cloudflare' -import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"; export default defineCloudflareConfig({ - incrementalCache: r2IncrementalCache, }); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ca78be6..147bc37 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -561,9 +561,9 @@ } }, "node_modules/@aws-sdk/client-dynamodb": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.864.0.tgz", - "integrity": "sha512-Z+8qCU8A8RKI/vaMZx3bUG3ZIvEBZzYRIEZA06Qx0QHttkDV/i2Q31Bs98Za/UV0aMXJYsgpHCvXeRgR7r8hYA==", + "version": "3.868.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.868.0.tgz", + "integrity": "sha512-a2uKLRplH3vTHgTHr+MaZ1YH0Sy0yVHK9rhM/drktxgXehMf4p7dZtt783c9SEupZvo46LxXryvwifoUdL4nRg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -7698,9 +7698,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -9364,9 +9364,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.6.tgz", - "integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", "license": "MIT", "dependencies": { "@noble/hashes": "1.8.0" @@ -11150,9 +11150,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", - "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -12386,9 +12386,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "funding": [ { "type": "opencollective", @@ -12558,9 +12558,9 @@ } }, "node_modules/cloudflare/node_modules/@types/node": { - "version": "18.19.122", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.122.tgz", - "integrity": "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA==", + "version": "18.19.123", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.123.tgz", + "integrity": "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -13022,9 +13022,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.200", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", - "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", + "version": "1.5.203", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz", + "integrity": "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==", "dev": true, "license": "ISC" }, @@ -14031,10 +14031,13 @@ } }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -15607,9 +15610,9 @@ } }, "node_modules/miniflare": { - "version": "4.20250813.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250813.0.tgz", - "integrity": "sha512-PsAGaNpdKXZvnaOvw2dpWWszhHtOX5ZwHLf7fEtW/g6QBSzdS707vFFbBBaew63hcpgo33CbuXZc0Z0P/5jNWQ==", + "version": "4.20250813.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250813.1.tgz", + "integrity": "sha512-6PyXwR4pZmH9ukO0jR5LmhlFVMktsVVGVcUjD9Lpev5QwnqjTRPEv73cnXCe0+oTbIm5TYnvXsAklaWxQuxstA==", "license": "MIT", "peer": true, "dependencies": { @@ -19127,9 +19130,9 @@ } }, "node_modules/wrangler": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.29.1.tgz", - "integrity": "sha512-PAGFQ6SS3fbpu0wrc4zO9wHYKWqIo7KmoAe66LGS3QdP3318O+dF1jL4d/kwNaj9Gh7HYQeGnTjeihqnhp9YWQ==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.30.0.tgz", + "integrity": "sha512-NXJUObuXxgG8/ChQ4yXkWLmDQ5ZcO98gyq1yFKYVntJ884C0IpDQrVnAv2RA0ZEz5eB8zal+4OKnr26P3N7ItA==", "license": "MIT OR Apache-2.0", "peer": true, "dependencies": { @@ -19137,7 +19140,7 @@ "@cloudflare/unenv-preset": "2.6.1", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", - "miniflare": "4.20250813.0", + "miniflare": "4.20250813.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.19", "workerd": "1.20250813.0" diff --git a/frontend/package.json b/frontend/package.json index cdb9d85..35a489e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,7 @@ "gen-lex": "lex-cli generate-main ../lexicon/uk/skyblur/*/*.json -o ./src/lexicon/UkSkyblur.ts", "build": "next build", "start": "next start", - "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", + "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview --ip 192.168.1.130 --port 4500", "deploy:preview": "opennextjs-cloudflare build && opennextjs-cloudflare deploy --env preview", "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy --env production", "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts", diff --git a/frontend/src/app/not-found.tsx b/frontend/src/app/not-found.tsx index ec5e4e7..8a18042 100644 --- a/frontend/src/app/not-found.tsx +++ b/frontend/src/app/not-found.tsx @@ -1,3 +1,4 @@ +'use client'; export default function NotFound() { return ( <> diff --git a/frontend/src/app/settings/main.tsx b/frontend/src/app/settings/main.tsx new file mode 100644 index 0000000..03f87a6 --- /dev/null +++ b/frontend/src/app/settings/main.tsx @@ -0,0 +1,429 @@ +"use client" +import Loading from "@/components/Loading"; +import URLCopyButton from "@/components/URLCopyButton"; +import { getPreference } from "@/logic/HandleBluesky"; +import { useLocaleStore } from "@/state/Locale"; +import { useXrpcAgentStore } from "@/state/XrpcAgent"; +import { ActorIdentifier, ResourceUri } from '@atcute/lexicons/syntax'; +import { Button, LoadingOverlay, Switch, Textarea, TextInput } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import Image from "next/image"; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useState } from "react"; +import { HiCheck } from "react-icons/hi"; +import { MdArrowBack } from "react-icons/md"; + +export default function Settings() { + const agent = useXrpcAgentStore((state) => state.agent); + const userProf = useXrpcAgentStore((state) => state.userProf); + const did = useXrpcAgentStore((state) => state.did); + const [isLoading, setIsLoading] = useState(true) + const [isUseMyPage, setIsUseMyPage] = useState(false) + const [preferenceMode, setPreferenceMode] = useState<"create" | "update">('create') + const [myPageDescription, setMyPageDescription] = useState('') + const [isCustomFeed, setIsCustomFeed] = useState(false) + const [isSave, setIsSave] = useState(false) + const locale = useLocaleStore((state) => state.localeData); + const [feedName, setFeedName] = useState(locale.Pref_CustomFeedDefaltName?.replace('{1}', did || '') || '') + const [feedDescription, setFeedDescription] = useState('') + const [feedUpdateMessage, setFeedUpdateMessage] = useState('') + const [feedAvatarImg, setFeedAvatarImg] = useState('') + const [feedAvatar, setFeedAvatar] = useState() + const router = useRouter(); + const [isMounted, setIsMounted] = useState(false); + const searchParams = useSearchParams(); + const lang = searchParams.get('lang') || 'ja'; + + useEffect(() => { + if (!agent || !did) { + router.push('/') + return + } + setIsMounted(true); + setIsLoading(true) + const fetchData = async () => { + try { + + const value = await getPreference(agent, did) + if (value === null) { + setPreferenceMode('create') + setIsLoading(false) + return + } + setPreferenceMode('update') + if (value.myPage.isUseMyPage) setIsUseMyPage(true) + setMyPageDescription(value.myPage.description || '') + + } catch (e) { + console.error(e) + } + + const feedATUri = 'at://' + did + '/app.bsky.feed.generator/skyblurCustomFeed' + const result = await agent.get('app.bsky.feed.getFeedGenerator', { + params: { + feed: feedATUri as ResourceUri, + }, + }); + + if (!result.ok) { + setFeedName(locale.Pref_CustomFeedDefaltName?.replace('{1}', userProf?.displayName || '').slice(0, 24) || '') + setFeedDescription('') + } else { + setFeedName(result.data.view.displayName) + setFeedDescription(result.data.view.description || '') + setFeedAvatarImg(result.data.view.avatar || '') + setIsCustomFeed(true) + + } + setIsLoading(false) + + }; + + fetchData(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [did]); + + + const changeFeedAvatar = (e: React.ChangeEvent) => { + if (!e.target.files) return; + + const imgObject = e.target.files[0]; + setFeedAvatar(imgObject) + if (imgObject) { + setFeedAvatarImg(window.URL.createObjectURL(imgObject)) + } else { + setFeedAvatarImg('') + + } + + }; + + const handleIsUseMyPage = async (param: boolean) => { + if (!agent || !did) return + setIsSave(true) + + try { + const writes = [ + { + $type: `com.atproto.repo.applyWrites#${preferenceMode}` as const, + collection: 'uk.skyblur.preference' as `${string}.${string}.${string}`, + rkey: 'self', + value: { + myPage: { + isUseMyPage: param, + description: myPageDescription, + }, + }, + }, + ]; + + await agent.post('com.atproto.repo.applyWrites', { + input: { + repo: did as ActorIdentifier, + writes: writes, + }, + }); + setIsUseMyPage(param) + + + } catch (e) { + console.error(e) + } + setIsSave(false) + + } + + + const submitFeedRecord = async () => { + if (!agent || !did) return + let avatarRef: Blob | null = null; + + let encoding: string = '' + + if (feedAvatar?.name.endsWith('png')) { + encoding = 'image/png' + } else if (feedAvatar?.name.endsWith('jpg') || feedAvatar?.name.endsWith('jpeg')) { + encoding = 'image/jpeg' + } else if (feedAvatar !== undefined) { + setFeedUpdateMessage(locale.Pref_FileType) + setIsLoading(false) + return + } + + try { + + if (feedAvatar) { + const fileUint = new Uint8Array(await feedAvatar.arrayBuffer()); + + const blobRes = await agent.post('com.atproto.repo.uploadBlob', { + input: fileUint, + encoding, + headers: { 'Content-Type': 'application/octet-stream' }, + }); + + if (!blobRes.ok) { + setFeedUpdateMessage('error'); + return + } + + avatarRef = blobRes.data.blob as unknown as Blob; + + } else if (feedAvatarImg) { + // feedAvatarImgからCIDと拡張子を取得 + let parts = feedAvatarImg.split('/'); + parts = parts[7].split('@'); + + const didValue = did as `did:${string}:${string}`; + + // BlobをGET + const ret = await agent.get('com.atproto.sync.getBlob', { + params: { + did: didValue, + cid: parts[0], + }, + as: 'blob', + }); + + if (!ret.ok) { + throw new Error('Failed to get blob'); + } + + const fileUint = new Uint8Array(await ret.data.arrayBuffer()); + + const blobRes = await agent.post('com.atproto.repo.uploadBlob', { + input: fileUint, + encoding, + headers: { 'Content-Type': 'application/octet-stream' }, + }); + + if (!blobRes.ok) { + setFeedUpdateMessage('error'); + return + } + + avatarRef = blobRes.data.blob as unknown as Blob; + } + + const record = { + did: 'did:web:feed.skyblur.uk', + displayName: feedName, + description: feedDescription, + createdAt: new Date().toISOString(), + ...(avatarRef ? { avatar: avatarRef } : {}), + }; + + await agent.post('com.atproto.repo.putRecord', { + input: { + repo: did as ActorIdentifier, + collection: 'app.bsky.feed.generator' as `${string}.${string}.${string}`, + rkey: 'skyblurCustomFeed', + record, + }, + }); + } catch (e) { + setFeedUpdateMessage('Error:' + e) + setIsSave(false) + return + } + + } + + const handleCustomFeed = async (param: boolean) => { + if (!agent || !did) return + + try { + if (param) { + await submitFeedRecord(); + } else { + const writes: { + $type: 'com.atproto.repo.applyWrites#delete'; + collection: 'app.bsky.feed.generator'; // 削除対象のコレクション名に変更 + rkey: string; + }[] = []; + + writes.push({ + $type: 'com.atproto.repo.applyWrites#delete', + collection: 'app.bsky.feed.generator', // 元の collection に合わせる + rkey: 'skyblurCustomFeed', // 削除したい rkey を指定 + }); + + await agent.post('com.atproto.repo.applyWrites', { + input: { + repo: did as ActorIdentifier, + writes, + }, + }); + + + } + + } catch (e) { + console.error(e) + } + + } + + + const handleSave = async () => { + if (!agent || !did) return + setFeedUpdateMessage("") + setIsSave(true) + + notifications.show({ + title: locale.Menu_Settings, + loading: true, + autoClose: false, + message: locale.Pref_SaveIsInProgress, + }); + try { + await handleCustomFeed(isCustomFeed) + await handleIsUseMyPage(isUseMyPage) + notifications.clean() + notifications.show({ + title: 'Success', + message: locale.Pref_SaveCompleted, + color: 'teal', + icon: + }); + router.push('/') + } catch (e) { + notifications.clean() + setFeedUpdateMessage('Error:' + e) + } + setIsSave(false) + + } + + + if (!isMounted) { + return ( + + ); + } + + return ( + < > +
+ <> +
+ {locale.Pref_Title} +
+
+ <> + <> + + {locale.Pref_MyPage} +
{locale.Pref_MyPagePublishDescription}
+
+ setIsUseMyPage(event.currentTarget.checked)} + + label={locale.Pref_MyPagePublish} + /> +
+ {isUseMyPage && ( + <> +
{locale.Pref_MyPageDesc}
+