Skip to content

Commit 96687cf

Browse files
committed
v1.2.36
1 parent 7abcd81 commit 96687cf

7 files changed

Lines changed: 298 additions & 42 deletions

File tree

assets/dist/main.js

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7300,7 +7300,7 @@ function $16a92ae9d4279a5e$var$showReloadPrompt(reason) {
73007300
buttons: 'confirm_cancel',
73017301
onConfirm: ()=>{
73027302
window.__comigoReloadPromptOpen = false;
7303-
window.location.reload();
7303+
$16a92ae9d4279a5e$var$reloadComigoPage();
73047304
},
73057305
onCancel: ()=>{
73067306
window.__comigoReloadPromptOpen = false;
@@ -7309,17 +7309,56 @@ function $16a92ae9d4279a5e$var$showReloadPrompt(reason) {
73097309
}
73107310
function $16a92ae9d4279a5e$var$autoReloadAfterLibraryRescan() {
73117311
if (!$16a92ae9d4279a5e$var$shouldShowUISuggestReloadPrompt() || window.__comigoAutoReloadQueued) return;
7312+
// 若本页正在发起重扫请求(rescan POST),先别 reload:
7313+
// 否则 location.reload() 会把这个还没返回的 fetch 直接 abort 掉,
7314+
// Firefox 报 “TypeError: NetworkError when attempting to fetch resource”,
7315+
// 让明明扫描成功的操作弹出“网络错误,请重试”。
7316+
// 改为挂起,等该 fetch 在自己的 finally 里结束后再刷新。
7317+
if (window.__comigoRescanInFlight) {
7318+
window.__comigoReloadPending = true;
7319+
return;
7320+
}
73127321
window.__comigoAutoReloadQueued = true;
7313-
window.location.reload();
7322+
$16a92ae9d4279a5e$var$reloadComigoPage();
7323+
}
7324+
// 由发起重扫的页面在 fetch 结束(finally)后调用:执行被挂起的整页刷新。
7325+
function $16a92ae9d4279a5e$var$comigoRunPendingReload() {
7326+
if (window.__comigoReloadPending && !window.__comigoAutoReloadQueued) {
7327+
window.__comigoReloadPending = false;
7328+
window.__comigoAutoReloadQueued = true;
7329+
$16a92ae9d4279a5e$var$reloadComigoPage();
7330+
}
73147331
}
73157332
function $16a92ae9d4279a5e$var$appendSharedLog(line) {
73167333
if (typeof window.__comigoLogAppend === 'function') window.__comigoLogAppend(line);
73177334
}
7335+
// 取消尚未执行的延迟连接,页面即将卸载时不能再新建 EventSource。
7336+
function $16a92ae9d4279a5e$var$clearQueuedComigoSSEStart() {
7337+
if (window.__comigoSSEStartTimer) {
7338+
clearTimeout(window.__comigoSSEStartTimer);
7339+
window.__comigoSSEStartTimer = null;
7340+
}
7341+
window.__comigoSSEStartQueued = false;
7342+
}
7343+
// 主动关闭当前 SSE;由 reload/pagehide 共用,避免卸载时遗留被浏览器标记为中断的长连接。
7344+
function $16a92ae9d4279a5e$var$closeComigoSSE() {
7345+
$16a92ae9d4279a5e$var$clearQueuedComigoSSEStart();
7346+
if (!window.__comigoSSEInstance) return;
7347+
try {
7348+
window.__comigoSSEInstance.close();
7349+
} catch (_) {}
7350+
window.__comigoSSEInstance = null;
7351+
}
7352+
function $16a92ae9d4279a5e$var$reloadComigoPage() {
7353+
$16a92ae9d4279a5e$var$closeComigoSSE();
7354+
window.location.reload();
7355+
}
73187356
function $16a92ae9d4279a5e$var$queueComigoSSEStart() {
73197357
if (window.__comigoSSEStartQueued) return;
73207358
window.__comigoSSEStartQueued = true;
73217359
const start = ()=>{
7322-
setTimeout(()=>{
7360+
window.__comigoSSEStartTimer = setTimeout(()=>{
7361+
window.__comigoSSEStartTimer = null;
73237362
window.__comigoSSEStartQueued = false;
73247363
$16a92ae9d4279a5e$var$comigoSSEInit();
73257364
}, 1000);
@@ -7364,7 +7403,11 @@ function $16a92ae9d4279a5e$var$comigoAttachSSEListeners(es) {
73647403
}
73657404
function $16a92ae9d4279a5e$var$comigoSSEInit() {
73667405
if (!$16a92ae9d4279a5e$var$shouldEnableComigoSSE()) return null;
7367-
if (window.__comigoSSEInstance) return window.__comigoSSEInstance;
7406+
if (window.__comigoSSEInstance) {
7407+
if (window.__comigoSSEInstance.readyState === EventSource.CLOSED) window.__comigoSSEInstance = null;
7408+
else return window.__comigoSSEInstance;
7409+
}
7410+
if (window.__comigoSSEStartQueued) return window.__comigoSSEInstance;
73687411
// 页面初次加载时稍后再连,避免浏览器把 SSE 长连接误报为加载中断。
73697412
if (document.readyState !== 'complete') {
73707413
$16a92ae9d4279a5e$var$queueComigoSSEStart();
@@ -7379,6 +7422,19 @@ function $16a92ae9d4279a5e$var$comigoSSEInit() {
73797422
return es;
73807423
}
73817424
window.__comigoSSEInit = $16a92ae9d4279a5e$var$comigoSSEInit;
7425+
window.__comigoRunPendingReload = $16a92ae9d4279a5e$var$comigoRunPendingReload;
7426+
// 全局启动 SSE;具体事件处理仍由上面的路径判断决定,阅读页不会被重扫通知打断。
7427+
$16a92ae9d4279a5e$var$queueComigoSSEStart();
7428+
// 页面卸载时主动关闭 SSE,并取消尚未执行的延迟启动,避免卸载过程中创建/留下
7429+
// 被中断(aborted)的 /api/sse 请求。
7430+
if (typeof window.addEventListener === 'function') {
7431+
window.addEventListener('pagehide', ()=>{
7432+
$16a92ae9d4279a5e$var$closeComigoSSE();
7433+
});
7434+
window.addEventListener('pageshow', ()=>{
7435+
$16a92ae9d4279a5e$var$queueComigoSSEStart();
7436+
});
7437+
}
73827438
window.dispatchEvent(new Event('comigo:sse-ready'));
73837439

73847440

assets/frontend/plugins/comigo_sse.js

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function showReloadPrompt(reason) {
6060
buttons: 'confirm_cancel',
6161
onConfirm: () => {
6262
window.__comigoReloadPromptOpen = false
63-
window.location.reload()
63+
reloadComigoPage()
6464
},
6565
onCancel: () => {
6666
window.__comigoReloadPromptOpen = false
@@ -72,8 +72,26 @@ function autoReloadAfterLibraryRescan() {
7272
if (!shouldShowUISuggestReloadPrompt() || window.__comigoAutoReloadQueued) {
7373
return
7474
}
75+
// 若本页正在发起重扫请求(rescan POST),先别 reload:
76+
// 否则 location.reload() 会把这个还没返回的 fetch 直接 abort 掉,
77+
// Firefox 报 “TypeError: NetworkError when attempting to fetch resource”,
78+
// 让明明扫描成功的操作弹出“网络错误,请重试”。
79+
// 改为挂起,等该 fetch 在自己的 finally 里结束后再刷新。
80+
if (window.__comigoRescanInFlight) {
81+
window.__comigoReloadPending = true
82+
return
83+
}
7584
window.__comigoAutoReloadQueued = true
76-
window.location.reload()
85+
reloadComigoPage()
86+
}
87+
88+
// 由发起重扫的页面在 fetch 结束(finally)后调用:执行被挂起的整页刷新。
89+
function comigoRunPendingReload() {
90+
if (window.__comigoReloadPending && !window.__comigoAutoReloadQueued) {
91+
window.__comigoReloadPending = false
92+
window.__comigoAutoReloadQueued = true
93+
reloadComigoPage()
94+
}
7795
}
7896

7997
function appendSharedLog(line) {
@@ -82,13 +100,40 @@ function appendSharedLog(line) {
82100
}
83101
}
84102

103+
// 取消尚未执行的延迟连接,页面即将卸载时不能再新建 EventSource。
104+
function clearQueuedComigoSSEStart() {
105+
if (window.__comigoSSEStartTimer) {
106+
clearTimeout(window.__comigoSSEStartTimer)
107+
window.__comigoSSEStartTimer = null
108+
}
109+
window.__comigoSSEStartQueued = false
110+
}
111+
112+
// 主动关闭当前 SSE;由 reload/pagehide 共用,避免卸载时遗留被浏览器标记为中断的长连接。
113+
function closeComigoSSE() {
114+
clearQueuedComigoSSEStart()
115+
if (!window.__comigoSSEInstance) {
116+
return
117+
}
118+
try {
119+
window.__comigoSSEInstance.close()
120+
} catch (_) {}
121+
window.__comigoSSEInstance = null
122+
}
123+
124+
function reloadComigoPage() {
125+
closeComigoSSE()
126+
window.location.reload()
127+
}
128+
85129
function queueComigoSSEStart() {
86130
if (window.__comigoSSEStartQueued) {
87131
return
88132
}
89133
window.__comigoSSEStartQueued = true
90134
const start = () => {
91-
setTimeout(() => {
135+
window.__comigoSSEStartTimer = setTimeout(() => {
136+
window.__comigoSSEStartTimer = null
92137
window.__comigoSSEStartQueued = false
93138
comigoSSEInit()
94139
}, 1000)
@@ -165,6 +210,13 @@ function comigoSSEInit() {
165210
return null
166211
}
167212
if (window.__comigoSSEInstance) {
213+
if (window.__comigoSSEInstance.readyState === EventSource.CLOSED) {
214+
window.__comigoSSEInstance = null
215+
} else {
216+
return window.__comigoSSEInstance
217+
}
218+
}
219+
if (window.__comigoSSEStartQueued) {
168220
return window.__comigoSSEInstance
169221
}
170222
// 页面初次加载时稍后再连,避免浏览器把 SSE 长连接误报为加载中断。
@@ -180,4 +232,20 @@ function comigoSSEInit() {
180232
}
181233

182234
window.__comigoSSEInit = comigoSSEInit
235+
window.__comigoRunPendingReload = comigoRunPendingReload
236+
237+
// 全局启动 SSE;具体事件处理仍由上面的路径判断决定,阅读页不会被重扫通知打断。
238+
queueComigoSSEStart()
239+
240+
// 页面卸载时主动关闭 SSE,并取消尚未执行的延迟启动,避免卸载过程中创建/留下
241+
// 被中断(aborted)的 /api/sse 请求。
242+
if (typeof window.addEventListener === 'function') {
243+
window.addEventListener('pagehide', () => {
244+
closeComigoSSE()
245+
})
246+
window.addEventListener('pageshow', () => {
247+
queueComigoSSEStart()
248+
})
249+
}
250+
183251
window.dispatchEvent(new Event('comigo:sse-ready'))

config/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package config
22

3-
var version = "v1.2.35"
3+
var version = "v1.2.36"
44

55
func GetVersion() string {
66
return version

templ/pages/settings/settings.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package settings
22

33
import (
4-
"path/filepath"
54
"regexp"
65
"strings"
76

@@ -10,6 +9,7 @@ import (
109
"github.com/yumenaka/comigo/config"
1110
"github.com/yumenaka/comigo/model"
1211
"github.com/yumenaka/comigo/templ/common"
12+
"github.com/yumenaka/comigo/tools"
1313
"github.com/yumenaka/comigo/tools/logger"
1414
"github.com/yumenaka/comigo/tools/service"
1515
"github.com/yumenaka/comigo/tools/tailscale_plugin"
@@ -50,6 +50,10 @@ func getTranslations(value string) string {
5050
func GetStoreBookCounts() map[string]int {
5151
counts := make(map[string]int)
5252

53+
// 设置页数量必须和首页一致:先清理已移除书库和源文件不存在的书籍。
54+
model.ClearBookWhenStoreUrlNotExist(config.GetCfg().StoreUrls)
55+
model.ClearBookNotExist()
56+
5357
// 获取所有书籍
5458
allBooks, err := model.IStore.ListBooks()
5559
if err != nil {
@@ -61,18 +65,26 @@ func GetStoreBookCounts() map[string]int {
6165
for _, book := range allBooks {
6266
// 只统计非书籍组的实际书籍
6367
if book.Type != model.TypeBooksGroup {
64-
// 将书库路径转换为绝对路径以便匹配
65-
storePathAbs, err := filepath.Abs(book.StoreUrl)
66-
if err != nil {
67-
storePathAbs = book.StoreUrl
68-
}
69-
counts[storePathAbs]++
68+
counts[storeBookCountKey(book.StoreUrl)]++
7069
}
7170
}
7271

7372
return counts
7473
}
7574

75+
// storeBookCountKey 统一设置页“配置路径”和“书籍 StoreUrl”的统计 key。
76+
// 本地路径转绝对 clean;远程 URL 保持原样,避免 filepath.Clean 破坏 URL。
77+
func storeBookCountKey(storeURL string) string {
78+
normalized, remote, err := tools.NormalizeStoreURLForCompare(storeURL)
79+
if err != nil {
80+
return storeURL
81+
}
82+
if remote {
83+
return storeURL
84+
}
85+
return normalized
86+
}
87+
7688
// PageHandler 设定页面
7789
func PageHandler(c echo.Context) error {
7890
tsStatus, err := tailscale_plugin.GetTailscaleStatus(c.Request().Context())
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package settings
2+
3+
import (
4+
"errors"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/yumenaka/comigo/config"
10+
"github.com/yumenaka/comigo/model"
11+
)
12+
13+
type storeBookCountsTestStore struct {
14+
books map[string]*model.Book
15+
deleteCalls int
16+
generateCalls int
17+
}
18+
19+
func (s *storeBookCountsTestStore) StoreBook(b *model.Book) error {
20+
s.books[b.BookID] = b
21+
return nil
22+
}
23+
24+
func (s *storeBookCountsTestStore) GetBook(id string) (*model.Book, error) {
25+
if b, ok := s.books[id]; ok {
26+
return b, nil
27+
}
28+
return nil, errors.New("book not found")
29+
}
30+
31+
func (s *storeBookCountsTestStore) DeleteBook(id string) error {
32+
s.deleteCalls++
33+
delete(s.books, id)
34+
return nil
35+
}
36+
37+
func (s *storeBookCountsTestStore) ListBooks() ([]*model.Book, error) {
38+
books := make([]*model.Book, 0, len(s.books))
39+
for _, book := range s.books {
40+
books = append(books, book)
41+
}
42+
return books, nil
43+
}
44+
45+
func (s *storeBookCountsTestStore) GenerateBookGroup() error {
46+
s.generateCalls++
47+
return nil
48+
}
49+
50+
func (s *storeBookCountsTestStore) StoreBookMark(mark *model.BookMark) error { return nil }
51+
52+
func (s *storeBookCountsTestStore) GetBookMarks(bookID string) (*model.BookMarks, error) {
53+
marks := model.BookMarks{}
54+
return &marks, nil
55+
}
56+
57+
func (s *storeBookCountsTestStore) DeleteBookMark(bookID string, markType model.MarkType, pageIndex int) error {
58+
return nil
59+
}
60+
61+
func TestGetStoreBookCountsCleansMissingBooks(t *testing.T) {
62+
oldCfg := config.CopyCfg()
63+
t.Cleanup(func() {
64+
*config.GetCfg() = oldCfg
65+
})
66+
oldStore := model.IStore
67+
t.Cleanup(func() {
68+
model.IStore = oldStore
69+
})
70+
71+
storeDir := t.TempDir()
72+
existingPath := filepath.Join(storeDir, "exists.zip")
73+
if err := os.WriteFile(existingPath, []byte("zip"), 0o644); err != nil {
74+
t.Fatalf("write fixture: %v", err)
75+
}
76+
missingPath := filepath.Join(storeDir, "missing.zip")
77+
config.GetCfg().StoreUrls = []string{storeDir}
78+
79+
testStore := &storeBookCountsTestStore{books: map[string]*model.Book{
80+
"existing": {
81+
BookInfo: model.BookInfo{
82+
BookID: "existing",
83+
BookPath: existingPath,
84+
StoreUrl: storeDir,
85+
Type: model.TypeZip,
86+
},
87+
},
88+
"missing": {
89+
BookInfo: model.BookInfo{
90+
BookID: "missing",
91+
BookPath: missingPath,
92+
StoreUrl: storeDir,
93+
Type: model.TypeZip,
94+
},
95+
},
96+
}}
97+
model.IStore = testStore
98+
99+
counts := GetStoreBookCounts()
100+
if got := counts[storeBookCountKey(storeDir)]; got != 1 {
101+
t.Fatalf("store count = %d, want 1 after missing book cleanup", got)
102+
}
103+
if testStore.deleteCalls != 1 {
104+
t.Fatalf("DeleteBook calls = %d, want 1", testStore.deleteCalls)
105+
}
106+
}

0 commit comments

Comments
 (0)