Skip to content

feat: Implement favicon upload and preprocessing (Closes #200) #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion client/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@
},
"favicon": {
"desc": "Set the icon (Favicon) displayed in the browser's address bar",
"title": "Favicon"
"title": "Favicon",
"update": {
"success": "Favicon update was successful",
"failed$message": "Favicon update failed, {{message}}"
}
},
"footer": {
"desc": "Set the footer content of the site (HTML)",
Expand Down
6 changes: 5 additions & 1 deletion client/public/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@
},
"favicon": {
"desc": "ブラウザのアドレスバーに表示されるアイコン(ファビコン)を設定します。",
"title": "ファビコン"
"title": "ファビコン",
"update": {
"success": "ファビコンの更新が成功しました",
"failed$message": "ファビコンの更新に失敗しました、{{message}}"
}
},
"footer": {
"desc": "サイトのフッターコンテンツを設定する(HTML)",
Expand Down
6 changes: 5 additions & 1 deletion client/public/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@
},
"favicon": {
"desc": "设置显示于浏览器的地址栏的图标(Favicon)",
"title": "网站图标"
"title": "网站图标",
"update": {
"success": "网站图标更新成功",
"failed$message": "网站图标更新失败,{{message}}"
}
},
"footer": {
"desc": "设置显示于站点底部的内容(HTML)",
Expand Down
6 changes: 5 additions & 1 deletion client/public/locales/zh-TW/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@
},
"favicon": {
"desc": "設定顯示於瀏覽器的地址欄的圖標(Favicon)",
"title": "網站圖標"
"title": "網站圖標",
"update": {
"success": "網站圖標更新成功",
"failed$message": "網站圖標更新失敗,{{message}}"
}
},
"footer": {
"desc": "設定顯示於網站底部的内容(HTML)",
Expand Down
4 changes: 2 additions & 2 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { getCookie } from 'typescript-cookie'
import { DefaultParams, PathPattern, Route, Switch } from 'wouter'
Expand Down Expand Up @@ -61,7 +61,7 @@ function App() {
}
ref.current = true
}, [])
const favicon = useMemo(() => config.get<string>("favicon"), [config])
const favicon = `${process.env.API_URL}/favicon`;
return (
<>
<ClientConfigContext.Provider value={config}>
Expand Down
108 changes: 93 additions & 15 deletions client/src/page/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,46 @@ export function Settings() {
ref.current = true;
}, []);

function onFileChange(e: ChangeEvent<HTMLInputElement>) {
async function handleFaviconChange(e: ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (file) {
client.wp.post({
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
if (file.size > MAX_FILE_SIZE) {
showAlert(
t("upload.failed$size", {
size: MAX_FILE_SIZE / 1024 / 1024,
}),
);
return;
}
await client.favicon
.post(
{
file: file,
},
{
headers: headersWithAuth(),
},
)
.then(({ data }) => {
if (data && typeof data !== "string") {
showAlert(t("settings.favicon.update.success"));
}
})
.catch((err) => {
showAlert(
t("settings.favicon.update.failed$message", {
message: err.message,
}),
);
});
}
}

async function onFileChange(e: ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (file) {
await client.wp.post({
data: file,
}, {
headers: headersWithAuth()
Expand Down Expand Up @@ -106,7 +142,13 @@ export function Settings() {
<ItemSwitch title={t('settings.comment.enable.title')} description={t('settings.comment.enable.desc')} type="client" configKey="comment.enabled" />
<ItemSwitch title={t('settings.counter.enable.title')} description={t('settings.counter.enable.desc')} type="client" configKey="counter.enabled" />
<ItemSwitch title={t('settings.rss.title')} description={t('settings.rss.desc')} type="client" configKey="rss" />
<ItemInput title={t('settings.favicon.title')} description={t('settings.favicon.desc')} type="client" configKey="favicon" configKeyTitle="Favicon" />
<ItemWithUpload
title={t("settings.favicon.title")}
description={t("settings.favicon.desc")}
// @see https://developers.cloudflare.com/images/transform-images/#supported-input-formats
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml"
onFileChange={handleFaviconChange}
/>
<ItemInput title={t('settings.footer.title')} description={t('settings.footer.desc')} type="client" configKey="footer" configKeyTitle="Footer HTML" />
<ItemButton title={t('settings.cache.clear.title')} description={t('settings.cache.clear.desc')} buttonTitle={t('clear')} onConfirm={async () => {
await client.config.cache.delete(undefined, {
Expand All @@ -119,6 +161,7 @@ export function Settings() {
})
}} alertTitle={t('settings.cache.clear.confirm.title')} alertDescription={t('settings.cache.clear.confirm.desc')} />
<ItemWithUpload title={t('settings.wordpress.title')} description={t('settings.wordpress.desc')}
accept="application/xml"
onFileChange={onFileChange} />
</div>
</main>
Expand Down Expand Up @@ -406,25 +449,60 @@ function ItemButton({
);
}

function ItemWithUpload({ title, description, onFileChange }: { title: string, description: string, onFileChange: (e: ChangeEvent<HTMLInputElement>) => void }) {
function ItemWithUpload({
title,
description,
accept,
onFileChange,
}: {
title: string;
description: string;
onFileChange: (e: ChangeEvent<HTMLInputElement>) => Promise<void>;
accept: string;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const [loading, setLoading] = useState(false);
const { t } = useTranslation();

const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
setLoading(true);
try {
await onFileChange(e);
} finally {
setLoading(false);
}
};

return (
<div className="flex flex-col w-full items-start">
<div className="flex flex-row justify-between w-full items-center">
<div className="flex flex-col">
<p className="text-lg font-bold dark:text-white">
{title}
</p>
<p className="text-xs text-neutral-500">
{description}
</p>
<p className="text-lg font-bold dark:text-white">{title}</p>
<p className="text-xs text-neutral-500">{description}</p>
</div>
<div className="flex flex-row items-center justify-center space-x-4">
{loading && (
<ReactLoading
width="1em"
height="1em"
type="spin"
color="#FC466B"
/>
)}
<input
ref={inputRef}
type="file"
className="hidden"
accept={accept}
onChange={handleFileChange}
/>
<Button
onClick={() => {
inputRef.current?.click();
}}
title={t("upload.title")}
/>
</div>
<input ref={inputRef} type="file" className="hidden" accept="application/xml"
onChange={onFileChange} />
<Button onClick={() => {
inputRef.current?.click();
}} title={t('upload.title')} />
</div>
</div>
);
Expand Down
2 changes: 0 additions & 2 deletions client/src/state/config.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { createContext } from "react";

const defaultFavicon = process.env.AVATAR ? `//wsrv.nl/?url=${encodeURIComponent(process.env.AVATAR)}&w=144&h=144&mask=circle` : '/favicon.ico';
export const defaultClientConfig = new Map(Object.entries({
"favicon": defaultFavicon,
"counter.enabled": true,
"friend_apply_enable": true,
"comment.enabled": true,
Expand Down
4 changes: 3 additions & 1 deletion server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import cors from '@elysiajs/cors';
import { serverTiming } from '@elysiajs/server-timing';
import { Elysia } from 'elysia';
import { CommentService } from './services/comments';
import { FaviconService } from "./services/favicon";
import { FeedService } from './services/feed';
import { FriendService } from './services/friends';
import { RSSService } from './services/rss';
Expand All @@ -28,6 +29,7 @@ export const app = () => new Elysia({ aot: false })
enabled: true,
}))
.use(UserService())
.use(FaviconService())
.use(FeedService())
.use(CommentService())
.use(TagService())
Expand All @@ -42,4 +44,4 @@ export const app = () => new Elysia({ aot: false })
return `${path} ${JSON.stringify(params)} not found`
})

export type App = ReturnType<typeof app>;
export type App = ReturnType<typeof app>;
Loading