Skip to content

Commit ebdaa6b

Browse files
committed
feat: add support for custom HTML template in server mode
- Updated Cargo.toml to include `image-png` feature and added `arboard` dependency. - Enhanced CLI to accept a custom HTML template path for server mode. - Modified server command to load and use the custom HTML template if provided. - Added logging functionality for client and server operations with structured log payloads. - Implemented commands to append, load, and clear logs, as well as to export the default HTML template. - Created a default server HTML template file for customization. - Updated services to generate server HTML content based on the custom template or fallback to the default.
1 parent 7d2a4d3 commit ebdaa6b

File tree

22 files changed

+854
-205
lines changed

22 files changed

+854
-205
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
| `--interval` | `-i` | `60` | 获取 hosts 的间隔时间(分钟) |
6868
| `--port` | `-p` | `9898` | 服务端模式监听端口 |
6969
| `--url` | `-u` | `https://hosts.gitcdn.top/hosts.txt` | 客户端模式远程 hosts 获取链接 |
70+
| `--template` | `-t` | 无(使用内置模板) | 服务端模式自定义 HTML 模板文件路径 |
7071
| `--lang` | `-l` | 自动检测 | 界面语言(`zh-CN``en-US``ja-JP`|
7172

7273
#### 启动客户端
@@ -96,8 +97,13 @@ fetch-github-hosts.exe -m server
9697

9798
# 自定义端口
9899
./fetch-github-hosts -m server -p 6666
100+
101+
# 自定义 HTML 模板文件
102+
./fetch-github-hosts -m server -t /path/to/template.html
99103
```
100104

105+
> 💡 自定义模板支持 `{{FGH_VERSION}}`(版本号)和 `{{FGH_UPDATE_TIME}}`(最近更新时间)两个模板变量
106+
101107
> 💡 不指定 `-m` 参数时将启动图形化界面
102108
103109
### 手动方式

README_EN.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Download your platform's binary from [Releases](https://github.com/Licoy/fetch-g
6767
| `--interval` | `-i` | `60` | Fetch interval in minutes |
6868
| `--port` | `-p` | `9898` | Server mode listening port |
6969
| `--url` | `-u` | `https://hosts.gitcdn.top/hosts.txt` | Client mode remote hosts URL |
70+
| `--template` | `-t` | None (built-in) | Server mode custom HTML template file path |
7071
| `--lang` | `-l` | Auto-detect | Language (`zh-CN`, `en-US`, `ja-JP`) |
7172

7273
#### Start Client
@@ -96,8 +97,13 @@ fetch-github-hosts.exe -m server
9697

9798
# Custom port
9899
./fetch-github-hosts -m server -p 6666
100+
101+
# Custom HTML template
102+
./fetch-github-hosts -m server -t /path/to/template.html
99103
```
100104

105+
> 💡 Custom templates support `{{FGH_VERSION}}` (version) and `{{FGH_UPDATE_TIME}}` (last update time) template variables
106+
101107
> 💡 Omitting the `-m` parameter launches the graphical user interface
102108
103109
### Manual Method

README_JA.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
| `--interval` | `-i` | `60` | hosts 取得間隔(分) |
6868
| `--port` | `-p` | `9898` | サーバーモードのリスニングポート |
6969
| `--url` | `-u` | `https://hosts.gitcdn.top/hosts.txt` | クライアントモードのリモート hosts URL |
70+
| `--template` | `-t` | なし(内蔵テンプレート) | サーバーモードのカスタム HTML テンプレートファイルパス |
7071
| `--lang` | `-l` | 自動検出 | 言語(`zh-CN``en-US``ja-JP`|
7172

7273
#### クライアント起動
@@ -96,8 +97,13 @@ fetch-github-hosts.exe -m server
9697

9798
# カスタムポート
9899
./fetch-github-hosts -m server -p 6666
100+
101+
# カスタム HTML テンプレート
102+
./fetch-github-hosts -m server -t /path/to/template.html
99103
```
100104

105+
> 💡 カスタムテンプレートは `{{FGH_VERSION}}`(バージョン)と `{{FGH_UPDATE_TIME}}`(最終更新日時)のテンプレート変数をサポートしています
106+
101107
> 💡 `-m` パラメータを省略するとグラフィカルインターフェースが起動します
102108
103109
### 手動設定

components/ClientMode.vue

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,17 @@
6767
:label="$t('common.start')"
6868
icon="i-heroicons-play"
6969
color="primary"
70+
:loading="isLoading"
71+
:disabled="isLoading"
7072
@click="startFetch"
7173
/>
7274
<UButton
7375
v-else
7476
:label="$t('common.stop')"
7577
icon="i-heroicons-stop"
7678
color="error"
79+
:loading="isLoading"
80+
:disabled="isLoading"
7781
@click="stopFetch"
7882
/>
7983
<UButton
@@ -93,18 +97,21 @@
9397
</div>
9498

9599
<!-- Log Viewer -->
96-
<LogViewer :logs="logs" class="flex-1 min-h-0" />
100+
<LogViewer :logs="logs" class="flex-1 min-h-0" @clear="clearLogs" />
97101
</div>
98102
</template>
99103

100104
<script setup lang="ts">
105+
import type { LogEntry } from './LogViewer.vue'
106+
101107
const { safeInvoke, safeListen } = useTauri()
102108
const { t } = useI18n()
103109
const toast = useToast()
104110
const { config: appConfig, loadConfig, updateClient } = useConfig()
105111
106112
const isRunning = ref(false)
107-
const logs = ref<string[]>([])
113+
const isLoading = ref(false)
114+
const logs = ref<LogEntry[]>([])
108115
109116
const hostsOrigins: Record<string, string> = {
110117
FetchGithubHosts: 'https://hosts.gitcdn.top/hosts.txt',
@@ -129,9 +136,21 @@ const hostsOriginOptions = Object.keys(hostsOrigins).map(k => ({
129136
value: k,
130137
}))
131138
132-
function addLog(msg: string) {
139+
function addLog(message: string, level: 'info' | 'success' | 'error' = 'info') {
133140
const now = new Date().toLocaleString()
134-
logs.value.unshift(`[${now}] ${msg}`)
141+
const entry: LogEntry = { time: now, message, level }
142+
logs.value.unshift(entry)
143+
// Persist log
144+
safeInvoke('append_log', {
145+
source: 'client',
146+
entry: JSON.stringify(entry),
147+
})
148+
}
149+
150+
/** Translate backend i18n log payload and add to logs */
151+
function addBackendLog(key: string, params: Record<string, any> | null, level: string) {
152+
const message = t(key, params || {})
153+
addLog(message, (level as 'info' | 'success' | 'error') || 'info')
135154
}
136155
137156
async function startFetch() {
@@ -150,23 +169,37 @@ async function startFetch() {
150169
return
151170
}
152171
172+
isLoading.value = true
153173
try {
154174
await safeInvoke('start_client', { url, interval })
155175
isRunning.value = true
156-
addLog(t('client.remoteUrlLog', { url }))
176+
isLoading.value = false
177+
addLog(t('client.remoteUrlLog', { url }), 'info')
157178
await syncToSharedConfig()
158179
} catch (e: any) {
159-
addLog(t('client.fetchFail', { error: e.toString() }))
180+
// Privilege escalation failed or other error: keep isRunning = false (Task 7)
181+
isRunning.value = false
182+
isLoading.value = false
183+
const msg = e?.toString() || ''
184+
if (msg.includes('USER_CANCELLED')) {
185+
toast.add({ title: t('common.cancelled'), color: 'warning' })
186+
addLog(t('common.cancelled'), 'error')
187+
} else {
188+
addLog(t('client.fetchFail', { error: msg }), 'error')
189+
}
160190
}
161191
}
162192
163193
async function stopFetch() {
194+
isLoading.value = true
164195
try {
165196
await safeInvoke('stop_client')
166197
isRunning.value = false
167-
addLog(t('client.fetchStop'))
198+
isLoading.value = false
199+
addLog(t('client.fetchStop'), 'info')
168200
} catch (e: any) {
169-
addLog(e.toString())
201+
isLoading.value = false
202+
addLog(e.toString(), 'error')
170203
}
171204
}
172205
@@ -183,18 +216,23 @@ async function flushDns() {
183216
try {
184217
const result = await safeInvoke<string>('flush_dns')
185218
toast.add({ title: t('client.flushDnsSuccess'), color: 'success' })
186-
addLog(result || t('client.flushDnsSuccess'))
219+
addLog(result || t('client.flushDnsSuccess'), 'success')
187220
} catch (e: any) {
188221
const msg = e?.toString() || ''
189222
if (msg.includes('USER_CANCELLED')) {
190223
toast.add({ title: t('common.cancelled'), color: 'warning' })
191224
} else {
192225
toast.add({ title: t('client.flushDnsFail') + ': ' + msg, color: 'error' })
193-
addLog(t('client.flushDnsFail') + ': ' + msg)
226+
addLog(t('client.flushDnsFail') + ': ' + msg, 'error')
194227
}
195228
}
196229
}
197230
231+
async function clearLogs() {
232+
logs.value = []
233+
await safeInvoke('clear_logs', { source: 'client' })
234+
}
235+
198236
function onAutoFetchChange(val: boolean) {
199237
config.autoFetch = val
200238
updateClient({
@@ -227,12 +265,32 @@ function syncFromSharedConfig() {
227265
}
228266
229267
onMounted(async () => {
230-
// Listen for log events from backend
231-
await safeListen<{ message: string }>('client-log', (event) => {
232-
addLog(event.payload.message)
268+
// Listen for log events from backend (now with i18n key + params + level)
269+
await safeListen<{ key: string; params?: Record<string, any>; level: string }>('client-log', (event) => {
270+
addBackendLog(event.payload.key, event.payload.params || null, event.payload.level)
233271
})
234272
await loadConfig()
235273
syncFromSharedConfig()
274+
275+
// Load persisted logs
276+
try {
277+
const persisted = await safeInvoke<string[]>('load_logs', { source: 'client' })
278+
if (persisted && persisted.length > 0) {
279+
// Load in reverse order (newest first, matching display order)
280+
for (const line of [...persisted].reverse()) {
281+
try {
282+
const entry = JSON.parse(line)
283+
if (entry.time && entry.message) {
284+
logs.value.push(entry)
285+
}
286+
} catch {
287+
// Legacy plain text log line — display as info
288+
logs.value.push({ time: '', message: line, level: 'info' })
289+
}
290+
}
291+
}
292+
} catch {}
293+
236294
// Auto fetch on startup if enabled
237295
if (config.autoFetch) {
238296
startFetch()

components/LogViewer.vue

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,39 @@
1818
<div
1919
v-for="(log, idx) in logs"
2020
:key="idx"
21-
class="opacity-70 py-0.5 border-b border-[var(--fgh-border)]/50 last:border-0"
21+
class="py-0.5 border-b border-[var(--fgh-border)]/50 last:border-0"
22+
:class="getLogClass(log)"
2223
>
23-
{{ log }}
24+
{{ getLogText(log) }}
2425
</div>
2526
</div>
2627
</div>
2728
</template>
2829

2930
<script setup lang="ts">
31+
export interface LogEntry {
32+
time: string
33+
message: string
34+
level: 'info' | 'success' | 'error'
35+
}
36+
3037
defineProps<{
31-
logs: string[]
38+
logs: (string | LogEntry)[]
3239
}>()
3340
3441
defineEmits(['clear'])
42+
43+
function getLogClass(log: string | LogEntry): string {
44+
if (typeof log === 'string') return 'opacity-70'
45+
switch (log.level) {
46+
case 'error': return 'text-red-500'
47+
case 'success': return 'text-green-500'
48+
default: return 'opacity-70'
49+
}
50+
}
51+
52+
function getLogText(log: string | LogEntry): string {
53+
if (typeof log === 'string') return log
54+
return `[${log.time}] ${log.message}`
55+
}
3556
</script>

0 commit comments

Comments
 (0)