GetCommandLineArguments();
18 |
19 | #endif // RUNNER_UTILS_H_
20 |
--------------------------------------------------------------------------------
/assets/images/ArrowBigLeft.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/ArrowBigRight.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/ArrowLeft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/ArrowLeft.png
--------------------------------------------------------------------------------
/assets/images/ArrowRight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/ArrowRight.png
--------------------------------------------------------------------------------
/assets/images/Prohibited28Filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/Prohibited28Filled.png
--------------------------------------------------------------------------------
/assets/images/SettingsOutline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/SettingsOutline.png
--------------------------------------------------------------------------------
/assets/images/SettingsOutline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/assets/images/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/audio.png
--------------------------------------------------------------------------------
/assets/images/bookmark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/error.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/error.jpg
--------------------------------------------------------------------------------
/assets/images/favicon-smail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/favicon-smail.png
--------------------------------------------------------------------------------
/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/favicon.ico
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/gowebly.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/home_directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/home_directory.png
--------------------------------------------------------------------------------
/assets/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/loading.gif
--------------------------------------------------------------------------------
/assets/images/loading.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/loading.jpg
--------------------------------------------------------------------------------
/assets/images/manifest-desktop-screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/manifest-desktop-screenshot.jpg
--------------------------------------------------------------------------------
/assets/images/manifest-mobile-screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/manifest-mobile-screenshot.jpg
--------------------------------------------------------------------------------
/assets/images/manifest-touch-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My PWA Project",
3 | "short_name": "My PWA Project",
4 | "description": "The PWA (Progressive Web App) part of the Gowebly project.",
5 | "background_color": "#FEFEF5",
6 | "theme_color": "#FEFEF5",
7 | "display": "standalone",
8 | "orientation": "portrait",
9 | "start_url": ".",
10 | "icons": [
11 | {
12 | "src": "manifest-touch-icon.svg",
13 | "type": "image/svg+xml",
14 | "sizes": "any"
15 | }
16 | ],
17 | "screenshots": [
18 | {
19 | "src": "manifest-desktop-screenshot.jpg",
20 | "sizes": "1280x720",
21 | "type": "image/jpeg",
22 | "form_factor": "wide",
23 | "label": "Desktop homescreen of My PWA Project"
24 | },
25 | {
26 | "src": "manifest-mobile-screenshot.jpg",
27 | "sizes": "720x1280",
28 | "type": "image/jpeg",
29 | "form_factor": "narrow",
30 | "label": "Mobile homescreen of My PWA Project"
31 | }
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/assets/images/not_found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/not_found.png
--------------------------------------------------------------------------------
/assets/images/oval.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/pdf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/pdf.png
--------------------------------------------------------------------------------
/assets/images/program_directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/program_directory.png
--------------------------------------------------------------------------------
/assets/images/puff.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/rar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/rar.png
--------------------------------------------------------------------------------
/assets/images/rings.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/images/tail-spin.svg:
--------------------------------------------------------------------------------
1 |
2 |
33 |
--------------------------------------------------------------------------------
/assets/images/three-dots.svg:
--------------------------------------------------------------------------------
1 |
2 |
34 |
--------------------------------------------------------------------------------
/assets/images/unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/unknown.png
--------------------------------------------------------------------------------
/assets/images/video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/video.png
--------------------------------------------------------------------------------
/assets/images/working_directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/working_directory.png
--------------------------------------------------------------------------------
/assets/images/zip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/zip.png
--------------------------------------------------------------------------------
/assets/main.js:
--------------------------------------------------------------------------------
1 | //此文件需要编译,编译指令请参考 package.json
2 | import 'htmx.org'
3 | import 'flowbite'
4 | // 基础插件
5 | import './plugins/i18n' // 这种 import 通常用于单纯执行该文件内的脚本或配置逻辑,确保它在程序启动或相关流程中被"触发"过。
6 | import './plugins/alpine'
7 | import './plugins/screenfull'
8 | // 声明各种变量
9 | import './stores/cookie_store'
10 | import './stores/global_store'
11 | import './stores/shelf_store'
12 | import './stores/scroll_store'
13 | import './stores/flip_store'
14 | import './stores/theme_store'
15 | import './utils/imageParameters'
16 |
17 | // Start Alpine.
18 | Alpine.start()
19 |
20 | // Document ready function to ensure the DOM is fully loaded.
21 | document.addEventListener('DOMContentLoaded', function () {
22 | initFlowbite() // initialize Flowbite
23 | })
24 |
25 | // Add event listeners for all HTMX events.
26 | document.body.addEventListener(
27 | 'htmx:afterSwap htmx:afterRequest htmx:afterSettle',
28 | function () {
29 | initFlowbite() // initialize Flowbite
30 | }
31 | )
--------------------------------------------------------------------------------
/assets/plugins/alpine.js:
--------------------------------------------------------------------------------
1 | import Alpine from 'alpinejs'
2 | import persist from '@alpinejs/persist'
3 | import morph from '@alpinejs/morph'
4 |
5 | window.Alpine = Alpine // 将 Alpine 实例添加到窗口对象中。
6 | Alpine.plugin(persist)
7 | Alpine.plugin(morph)
--------------------------------------------------------------------------------
/assets/plugins/i18n.js:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next'
2 | import LanguageDetector from 'i18next-browser-languagedetector'
3 | import enLocale from '../locale/en_US.json'
4 | import zhLocale from '../locale/zh_CN.json'
5 | import jaLocale from '../locale/ja_JP.json'
6 |
7 | i18next
8 | .use(LanguageDetector)
9 | .init({
10 | debug: false,
11 | initImmediate: true,
12 | supportedLngs: ['en-US', 'ja-JP', 'zh-CN', 'en', 'zh', 'ja'],
13 | fallbackLng: ['en', 'zh', 'ja'],
14 | resources: {
15 | 'en-US': {
16 | translation: enLocale,
17 | },
18 | en: {
19 | translation: enLocale,
20 | },
21 | 'zh-CN': {
22 | translation: zhLocale,
23 | },
24 | zh: {
25 | translation: zhLocale,
26 | },
27 | 'ja-JP': {
28 | translation: jaLocale,
29 | },
30 | ja: {
31 | translation: jaLocale,
32 | },
33 | },
34 | })
35 |
36 | window.i18next = i18next // 使i18next在全局作用域中可用
--------------------------------------------------------------------------------
/assets/plugins/screenfull.js:
--------------------------------------------------------------------------------
1 | import screenfull from 'screenfull'
2 |
3 | window.Screenfull = screenfull // 将 screenfull 实例添加到窗口对象中。
--------------------------------------------------------------------------------
/assets/script/flip.css:
--------------------------------------------------------------------------------
1 | /*此文件静态导入,不需要编译*/
2 | /* CSS 过渡 */
3 | /* https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_transitions/Using_CSS_transitions */
4 | #header, #StepsRangeArea {
5 | opacity: 1;
6 | transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
7 | }
8 |
9 | #header.hidden, #StepsRangeArea.hidden {
10 | opacity: 0;
11 | }
12 |
13 | /* 漫画div */
14 | .manga_area {
15 | /* 不可以被选中 */
16 | user-select: none;
17 | /* 火狐 */
18 | -moz-user-select: none;
19 | /* 谷歌 */
20 | -webkit-user-select: none;
21 | }
22 |
23 | /* 最后的一或两张图片*/
24 | .manga_area img {
25 | /* 不可以被选中 */
26 | user-select: none;
27 | /* 火狐 */
28 | -moz-user-select: none;
29 | /* 谷歌 */
30 | -webkit-user-select: none;
31 | }
32 |
--------------------------------------------------------------------------------
/assets/script/flip_sketch.js:
--------------------------------------------------------------------------------
1 | //此文件静态导入,不需要编译
2 | 'use strict'
3 |
4 |
5 |
--------------------------------------------------------------------------------
/assets/script/shelf.js:
--------------------------------------------------------------------------------
1 | //此文件静态导入,不需要编译
--------------------------------------------------------------------------------
/assets/stores/cookie_store.js:
--------------------------------------------------------------------------------
1 | // Alpine 使用 Persist 插件,用会话 cookie 作为存储
2 | // https://alpinejs.dev/plugins/persist#custom-storage
3 | // 定义自定义存储对象,公开 getItem 函数和 setItem 函数
4 | window.cookieStorage = {
5 | getItem(key) {
6 | let cookies = document.cookie.split(";");
7 | for (let i = 0; i < cookies.length; i++) {
8 | let cookie = cookies[i].split("=");
9 | if (key === cookie[0].trim()) {
10 | return decodeURIComponent(cookie[1]);
11 | }
12 | }
13 | return null;
14 | },
15 | setItem(key, value) {
16 | document.cookie = `${key}=${encodeURIComponent(value)}; SameSite=Lax`;//SameSite设置默认值(Lax),防止控制台报错。加载图像或框架(frame)的请求将不会包含用户的 Cookie。
17 | }
18 | }
19 |
20 | // // 然后就可以这样使用使用 cookieStorage 作为 Persist 插件的存储了
21 | // Alpine.store('cookie', {
22 | // someCookieKey: Alpine.$persist(false).using(cookieStorage).as('someCookieKey'),
23 | // })
--------------------------------------------------------------------------------
/assets/stores/flip_store.js:
--------------------------------------------------------------------------------
1 | // Flip 翻页模式
2 | Alpine.store('flip', {
3 | nowPageNum: 1,
4 | allPageNum: 100,
5 | imageMaxWidth: 400,
6 | //自动隐藏工具条
7 | autoHideToolbar: Alpine.$persist(false).as('flip.autoHideToolbar'),
8 | //自动对齐
9 | autoAlign: Alpine.$persist(true).as('flip.autoAlignTop'),
10 | //是否显示页头
11 | show_header: Alpine.$persist(true).as('flip.show_header'),
12 | //是否显示页脚
13 | showFooter: Alpine.$persist(true).as('flip.showFooter'),
14 | //是否显示页数
15 | showPageNum: Alpine.$persist(true).as('flip.showPageNum'),
16 | //是否是日本漫画【右半屏翻页,从左到右(true)】【右半屏翻页,从右到左(false)】
17 | mangaMode: Alpine.$persist(true).as('flip.mangaMode'),
18 | //swipeTurn or clickTurn
19 | swipeTurn: Alpine.$persist(true).as('flip.swipeTurn'),
20 | //双页模式
21 | doublePageMode: Alpine.$persist(false).as('flip.doublePageMode'),
22 | //自动拼合双页(TODO)
23 | autoDoublePageMode: Alpine.$persist(false).as(
24 | 'flip.autoDoublePageModeFlag'
25 | ),
26 | //是否保存阅读进度(页数)
27 | saveReadingProgress: Alpine.$persist(true).as('flip.saveReadingProgress'),
28 | //素描模式标记
29 | sketchModeFlag: false,
30 | //是否显示素描提示
31 | showPageHint: Alpine.$persist(false).as(
32 | 'flip.showPageHint'
33 | ),
34 | //翻页间隔时间
35 | sketchFlipSecond: 30,
36 | //计时用,从0开始
37 | sketchSecondCount: 0,
38 | })
--------------------------------------------------------------------------------
/assets/stores/scroll_store.js:
--------------------------------------------------------------------------------
1 | // Scroll 卷轴模式
2 | Alpine.store("scroll", {
3 | nowPageNum: 1,
4 | simplifyTitle: Alpine.$persist(true).as("scroll.simplifyTitle"), //是否简化标题
5 | //下拉模式下,漫画页面的底部间距。单位px。
6 | marginBottomOnScrollMode: Alpine.$persist(0).as(
7 | "scroll.marginBottomOnScrollMode",
8 | ),
9 | //卷轴模式下,是否分页加载(反之则无限下拉)
10 | fixedPagination: Alpine.$persist(false).as("scroll.fixedPagination"),
11 | // 卷轴模式的同步滚动,目前还没做
12 | syncScrollFlag: Alpine.$persist(false).as("scroll.syncScrollFlag"),
13 | imageMaxWidth: 400,
14 | // 屏幕宽横比,inLandscapeMode的判断依据
15 | aspectRatio: 1.2,
16 | // 可见范围宽高的具体值
17 | clientWidth: 0,
18 | clientHeight: 0,
19 | //漫画页的单位,是否使用固定值
20 | widthUseFixedValue: Alpine.$persist(true).as("scroll.widthUseFixedValue"),
21 | portraitWidthPercent: Alpine.$persist(100).as("scroll.portraitWidthPercent"),
22 | //横屏(Landscape)状态的漫画页宽度,百分比
23 | singlePageWidth_Percent: Alpine.$persist(60).as(
24 | "scroll.singlePageWidth_Percent",
25 | ),
26 | doublePageWidth_Percent: Alpine.$persist(95).as(
27 | "scroll.doublePageWidth_Percent",
28 | ),
29 | //横屏(Landscape)状态的漫画页宽度。px。
30 | singlePageWidth_PX: Alpine.$persist(720).as("scroll.singlePageWidth_PX"),
31 | doublePageWidth_PX: Alpine.$persist(1200).as("scroll.doublePageWidth_PX"),
32 | //书籍数据,需要从远程拉取
33 | //是否显示顶部页头
34 | showHeaderFlag: true,
35 | //是否显示页数
36 | showPageNum: Alpine.$persist(false).as("scroll.showPageNum"),
37 | //ws翻页相关
38 | syncPageByWS: Alpine.$persist(false).as("scroll.syncPageByWS"), //是否通过websocket同步翻页
39 | });
40 |
--------------------------------------------------------------------------------
/assets/stores/shelf_store.js:
--------------------------------------------------------------------------------
1 | // BookShelf 书架设置
2 | Alpine.store('shelf', {
3 | bookCardMode: Alpine.$persist('gird').as('shelf.bookCardMode'), //gird,list,text
4 | showFilename: Alpine.$persist(true).as('shelf.showFilename'), //是否显示文件名
5 | showFileIcon: Alpine.$persist(true).as('shelf.showFileIcon'), //是否显示文件图标
6 | simplifyTitle: Alpine.$persist(true).as('shelf.simplifyTitle'), //是否简化标题
7 | InfiniteDropdown: Alpine.$persist(false).as('shelf.InfiniteDropdown'), //卷轴模式下,是否无限下拉
8 | bookCardShowTitleFlag: Alpine.$persist(true).as('shelf.bookCardShowTitleFlag'), // 书库中的书籍是否显示文字版标题
9 | syncScrollFlag: false, // 同步滚动,目前还没做
10 | // 屏幕宽横比,inLandscapeMode的判断依据
11 | aspectRatio: 1.2,
12 | // 可见范围宽高的具体值
13 | clientWidth: 0,
14 | clientHeight: 0,
15 | })
--------------------------------------------------------------------------------
/assets/stores/theme_store.js:
--------------------------------------------------------------------------------
1 | // 自定义主题
2 | Alpine.store('theme', {
3 | theme: Alpine.$persist('light').as('theme'),
4 | interfaceColor: '#F5F5E4',
5 | backgroundColor: '#E0D9CD',
6 | textColor: '#000000',
7 | toggleTheme() {
8 | this.theme = this.theme === 'light' ? 'dark' : 'light'
9 | },
10 | })
--------------------------------------------------------------------------------
/assets/utils/imageParameters.js:
--------------------------------------------------------------------------------
1 | //请求图片文件时,可添加的额外参数
2 | const imageParameters = {
3 | resize_width: -1, // 缩放图片,指定宽度
4 | resize_height: -1, // 指定高度,缩放图片
5 | do_compress_image: false,
6 | resize_max_width: 800, //图片宽度大于这个上限时缩小
7 | resize_max_height: -1, //图片高度大于这个上限时缩小
8 | do_auto_crop: false,
9 | auto_crop_num: 1, // 自动切白边阈值,范围是0~100,其实为1就够了
10 | gray: false, //黑白化
11 | };
12 |
13 | //添加各种字符串参数,不需要的话为空
14 | const resize_width_str =
15 | imageParameters.resize_width > 0
16 | ? "&resize_width=" + imageParameters.resize_width
17 | : "";
18 | const resize_height_str =
19 | imageParameters.resize_height > 0
20 | ? "&resize_height=" + imageParameters.resize_height
21 | : "";
22 | const gray_str = imageParameters.gray ? "&gray=true" : "";
23 | const do_compress_image_str = imageParameters.do_compress_image
24 | ? "&resize_max_width=" + imageParameters.resize_max_width
25 | : "";
26 | const resize_max_height_str =
27 | imageParameters.resize_max_height > 0
28 | ? "&resize_max_height=" + imageParameters.resize_max_height
29 | : "";
30 | const auto_crop_str = imageParameters.do_auto_crop
31 | ? "&auto_crop=" + imageParameters.auto_crop_num
32 | : "";
33 |
34 | //所有附加的转换参数
35 | let addStr =
36 | resize_width_str +
37 | resize_height_str +
38 | do_compress_image_str +
39 | resize_max_height_str +
40 | auto_crop_str +
41 | gray_str;
42 |
43 | if (addStr!=="") {
44 | addStr = "?" + addStr.substring(1);
45 | console.log("addStr:", addStr);
46 | }
47 |
48 | export { addStr, imageParameters };
--------------------------------------------------------------------------------
/cmd/comi/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/yumenaka/comigo/cmd"
5 | "github.com/yumenaka/comigo/routers"
6 | )
7 |
8 | func main() {
9 | // 初始化命令行flag,环境变量与配置文件
10 | cmd.Execute()
11 | // 启动网页服务器(不阻塞)
12 | routers.StartWebServer()
13 | // 扫描书库(命令行指定)
14 | cmd.ScanStore(cmd.Args)
15 | // 在命令行显示QRCode
16 | cmd.ShowQRCode()
17 | // 退出时清理临时文件的处理函数
18 | cmd.SetShutdownHandler()
19 | }
20 |
--------------------------------------------------------------------------------
/cmd/experiments/switch_port.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "log"
7 | "net/http"
8 | "sync"
9 | "time"
10 |
11 | "github.com/labstack/echo/v4"
12 | )
13 |
14 | var (
15 | server *http.Server
16 | mutex sync.Mutex
17 | )
18 |
19 | // startServer 启动一个在指定端口监听的HTTP服务器
20 | func startServer(port string) {
21 | e := echo.New()
22 | e.GET("/", func(c echo.Context) error {
23 | return c.String(http.StatusOK, "Listening on port "+port)
24 | })
25 |
26 | mutex.Lock()
27 | server = &http.Server{
28 | Addr: ":" + port,
29 | Handler: e,
30 | }
31 | mutex.Unlock()
32 |
33 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
34 | log.Fatalf("listen: %s\n", err)
35 | }
36 | }
37 |
38 | // stopServer 停止当前的HTTP服务器
39 | func stopServer() error {
40 | mutex.Lock()
41 | defer mutex.Unlock()
42 | if server == nil {
43 | return nil
44 | }
45 |
46 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
47 | defer cancel()
48 |
49 | if err := server.Shutdown(ctx); err != nil {
50 | return err
51 | }
52 | server = nil
53 | return nil
54 | }
55 |
56 | // switchPort 停止当前的服务器并在新的端口上重新启动
57 | func switchPort(newPort string) {
58 | if err := stopServer(); err != nil {
59 | log.Fatalf("Server Shutdown Failed:%+v", err)
60 | }
61 | log.Println("Server Shutdown Successfully", "Starting Server...", "on port", newPort, "...")
62 | go startServer(newPort)
63 | }
64 |
65 | func main() {
66 | // 在端口8080上启动服务器
67 | go startServer("18080")
68 |
69 | // 假设在一段时间后需要切换到端口8081
70 | time.Sleep(20 * time.Second)
71 | switchPort("18081")
72 |
73 | // 为了示例,我们在这里阻塞主线程
74 | // 在实际应用中,需要一个更复杂的逻辑来决定何时停止程序
75 | select {}
76 | }
77 |
--------------------------------------------------------------------------------
/cmd/image_viewer/Readme.md:
--------------------------------------------------------------------------------
1 |
2 | ## 一个简单的在线图片浏览器
3 | ## Sqlite的部分实现有问题,暂时无法使用
4 | ## 接下来将扫描与缓存数据的部分应用到主项目中
5 |
6 | go generate ./ent
--------------------------------------------------------------------------------
/cmd/image_viewer/ent/predicate/predicate.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package predicate
4 |
5 | import (
6 | "entgo.io/ent/dialect/sql"
7 | )
8 |
9 | // Directory is the predicate function for directory builders.
10 | type Directory func(*sql.Selector)
11 |
12 | // Image is the predicate function for image builders.
13 | type Image func(*sql.Selector)
14 |
--------------------------------------------------------------------------------
/cmd/image_viewer/ent/runtime.go:
--------------------------------------------------------------------------------
1 | // +build tools
2 | //go:build tools && tools
3 | // +build tools,tools
4 |
5 | // Code generated by ent, DO NOT EDIT.
6 |
7 | package ent
8 |
9 | import (
10 | "github.com/yumenaka/comigo/cmd/image_viewer/ent/directory"
11 | "github.com/yumenaka/comigo/cmd/image_viewer/ent/image"
12 | "github.com/yumenaka/comigo/cmd/image_viewer/ent/schema"
13 | )
14 |
15 | // The init function reads all schema descriptors with runtime code
16 | // (default values, validators, hooks and policies) and stitches it
17 | // to their package variables.
18 | func init() {
19 | directoryFields := schema.Directory{}.Fields()
20 | _ = directoryFields
21 | // directoryDescPath is the schema descriptor for path field.
22 | directoryDescPath := directoryFields[0].Descriptor()
23 | // directory.PathValidator is a validator for the "path" field. It is called by the builders before save.
24 | directory.PathValidator = directoryDescPath.Validators[0].(func(string) error)
25 | // directoryDescName is the schema descriptor for name field.
26 | directoryDescName := directoryFields[1].Descriptor()
27 | // directory.NameValidator is a validator for the "name" field. It is called by the builders before save.
28 | directory.NameValidator = directoryDescName.Validators[0].(func(string) error)
29 | imageFields := schema.Image{}.Fields()
30 | _ = imageFields
31 | // imageDescPath is the schema descriptor for path field.
32 | imageDescPath := imageFields[0].Descriptor()
33 | // image.PathValidator is a validator for the "path" field. It is called by the builders before save.
34 | image.PathValidator = imageDescPath.Validators[0].(func(string) error)
35 | // imageDescName is the schema descriptor for name field.
36 | imageDescName := imageFields[1].Descriptor()
37 | // image.NameValidator is a validator for the "name" field. It is called by the builders before save.
38 | image.NameValidator = imageDescName.Validators[0].(func(string) error)
39 | }
40 |
--------------------------------------------------------------------------------
/cmd/image_viewer/ent/runtime/runtime.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package runtime
4 |
5 | // The schema-stitching logic is generated in github.com/yumenaka/comigo/cmd/image_viewer/ent/runtime.go
6 |
7 | const (
8 | Version = "v0.14.3" // Version of ent codegen.
9 | Sum = "h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=" // Sum of ent codegen.
10 | )
11 |
--------------------------------------------------------------------------------
/cmd/image_viewer/ent/schema/directory.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | )
8 |
9 | // Directory 模型定义(目录)
10 | type Directory struct {
11 | ent.Schema
12 | }
13 |
14 | // Fields 定义 Directory 的字段
15 | func (Directory) Fields() []ent.Field {
16 | return []ent.Field{
17 | field.String("path").NotEmpty().Unique(),
18 | field.String("name").NotEmpty(),
19 | }
20 | }
21 |
22 | // Edges 定义 Directory 的关系(父子目录,自身关联;以及与 Image 的关系)
23 | func (Directory) Edges() []ent.Edge {
24 | return []ent.Edge{
25 | edge.To("children", Directory.Type).From("parent").Unique(), // 子目录列表,唯一父目录
26 | edge.To("images", Image.Type), // 关联的图片列表
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/image_viewer/ent/schema/image.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "entgo.io/ent"
5 | "entgo.io/ent/schema/edge"
6 | "entgo.io/ent/schema/field"
7 | )
8 |
9 | // Image 模型定义(图片文件)
10 | type Image struct {
11 | ent.Schema
12 | }
13 |
14 | // Fields 定义 Image 的字段
15 | func (Image) Fields() []ent.Field {
16 | return []ent.Field{
17 | field.String("path").NotEmpty().Unique(),
18 | field.String("name").NotEmpty(),
19 | field.Int64("size"),
20 | field.Time("mod_time"),
21 | field.Time("create_time"),
22 | }
23 | }
24 |
25 | // Edges 定义 Image 与 Directory 的关系
26 | func (Image) Edges() []ent.Edge {
27 | return []ent.Edge{
28 | edge.From("directory", Directory.Type).Ref("images").Unique().Required(),
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/image_viewer/models.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/yumenaka/comigo/model"
4 |
5 | // 支持的图片文件扩展名(统一用小写比较)
6 | var imageExtensions = map[string]bool{
7 | ".jpg": true,
8 | ".jpeg": true,
9 | ".png": true,
10 | ".gif": true,
11 | ".webp": true,
12 | }
13 |
14 | // DirNode 表示目录树节点,用于 JSON 存储模式
15 | type DirNode struct {
16 | Name string `json:"name"`
17 | Path string `json:"path"`
18 | SubDirs []DirNode `json:"sub_dirs"` // 子目录列表
19 | Files []model.MediaFileInfo `json:"files"` // 本目录下的图片文件列表
20 | }
21 |
22 | // ListResponse 用于 API 返回目录内容(子目录和文件),支持分页
23 | type ListResponse struct {
24 | Directories []DirectoryInfo `json:"directories"`
25 | Images []model.MediaFileInfo `json:"images"`
26 | TotalImages int `json:"total_images,omitempty"` // 符合条件的图片总数(用于分页)
27 | Page int `json:"page,omitempty"`
28 | PageSize int `json:"page_size,omitempty"`
29 | }
30 |
31 | // DirectoryInfo 用于 API 输出的子目录基本信息
32 | type DirectoryInfo struct {
33 | Name string `json:"name"`
34 | Path string `json:"path"`
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/image_viewer/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/yumenaka/comigo/model"
4 |
5 | // 辅助函数:检查字符串是否在 slice 中
6 | func stringInSlice(s string, list []string) bool {
7 | for _, v := range list {
8 | if v == s {
9 | return true
10 | }
11 | }
12 | return false
13 | }
14 |
15 | // 辅助函数:检查路径是否在文件列表中(根据 MediaFileInfo.Path)
16 | func pathInList(path string, files []model.MediaFileInfo) bool {
17 | for _, f := range files {
18 | if f.Path == path {
19 | return true
20 | }
21 | }
22 | return false
23 | }
24 |
25 | // 辅助函数:在 DirNode 树中找到指定路径的节点
26 | func findDirNode(root DirNode, targetPath string) *DirNode {
27 | if root.Path == targetPath {
28 | return &root
29 | }
30 | for i := range root.SubDirs {
31 | if root.SubDirs[i].Path == targetPath {
32 | return &root.SubDirs[i]
33 | }
34 | // 递归在子目录中查找
35 | if result := findDirNode(root.SubDirs[i], targetPath); result != nil {
36 | return result
37 | }
38 | }
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/cmd/mobile/Readme.md:
--------------------------------------------------------------------------------
1 | ## Android
2 | ```
3 | gomobile bind -ldflags="-w -s" -o ../../app/android/app/libs/server.aar -target=android -androidapi 21 -javapkg="net.yumenaka.comigo" github.com/yumenaka/comigo/cmd/mobile
4 | ```
5 |
6 | ## IOS
7 | ```
8 | gomobile bind -ldflags="-w -s" -o ../app/ios/Frameworks/server.xcframework -target=ios github.com/yumenaka/comigo/cmd/mobile
9 | ```
10 |
11 | ## MacOS
12 | ```
13 | go build -ldflags="-w -s" -buildmode=c-shared -o _temp/output/libserver.dylib github.com/yumenaka/comigo/cmd/desktop
14 | cp _temp/output/libserver.h ../app/include/
15 | cp _temp/output/libserver.dylib ../app/macos/Frameworks/
16 | ```
17 |
18 | ## Linux
19 | ```
20 | go build -ldflags="-w -s" -buildmode=c-shared -o libserver.so github.com/yumenaka/comigo/cmd/desktop
21 | ```
--------------------------------------------------------------------------------
/cmd/mobile/main.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "os"
5 | "strconv"
6 |
7 | "github.com/yumenaka/comigo/cmd"
8 | "github.com/yumenaka/comigo/config"
9 | "github.com/yumenaka/comigo/routers"
10 | // _ "golang.org/x/mobile/bind"
11 | )
12 |
13 | func Start(path string) (string, error) {
14 | // 初始化命令行flag,环境变量与配置文件
15 | cmd.Execute()
16 | // 扫描书库(命令行指定)
17 | cmd.ScanStore(os.Args)
18 | // 启动网页服务器(不阻塞)
19 | routers.StartWebServer()
20 | return strconv.Itoa(config.GetCfg().Port), nil
21 | }
22 |
--------------------------------------------------------------------------------
/cmd/set_daemon.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 |
7 | "github.com/sevlyar/go-daemon"
8 | "github.com/yumenaka/comigo/config"
9 | "github.com/yumenaka/comigo/util/logger"
10 | )
11 |
12 | // DemonFlag TODO: 实测macos可用,正确地实装,需要理解守护进程的概念
13 | // 需要去 cmd/init_flags.go 设置flag
14 | var (
15 | DemonFlag bool
16 | StopDaemonFlag bool
17 | )
18 |
19 | // SetDaemon 设置守护进程, To terminate the daemon use: kill `cat comigo.pid`
20 | // 该函数会在Unix系统上将当前进程转化为守护进程,并在后台运行。
21 | // 如果当前系统不是Unix系统,或者没有指定启动或停止守护进程的参数,则直接返回。
22 | // https://github.com/sevlyar/go-daemon
23 | // https://github.com/sevlyar/go-daemon/blob/v0.1.6/examples/cmd/gd-simple/simple.go
24 | // go run main.go --start
25 | // go run main.go --stop
26 | func SetDaemon() {
27 | // 如果不是unix系统,直接返回
28 | if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
29 | return
30 | }
31 | // 如果没有指定启动或停止守护进程的参数,直接返回
32 | if !DemonFlag && !StopDaemonFlag {
33 | return
34 | }
35 | cntxt := &daemon.Context{
36 | PidFileName: "/var/run/comigo.pid",
37 | PidFilePerm: 0o644,
38 | LogFileName: "comigo.log",
39 | LogFilePerm: 0o640,
40 | WorkDir: "./",
41 | Umask: 0o27,
42 | Args: []string{fmt.Sprintf("[comigo %s daemon]", config.GetVersion())},
43 | }
44 | // Reborn 会在指定的上下文中启动当前进程的第二个副本。
45 | // 该函数在子进程和父进程中分别执行不同的代码段,并对子进程进行守护化(daemonization)。
46 | // 它看起来类似于 fork-daemonization,但对 goroutine 是安全的。
47 | // 调用成功时,父进程返回一个 *os.Process 对象,子进程返回 nil;否则返回错误。
48 | child, err := cntxt.Reborn()
49 | if err != nil {
50 | logger.Fatal("Unable to run: ", err)
51 | }
52 | // 父运行运行到这里,child不等于nil,然后会返回
53 | if child != nil {
54 | return
55 | } else {
56 | // 子进程运行到这里,child等于nil
57 | logger.Info("- - - - - - - - - - - - - - -")
58 | logger.Info("child daemon started?")
59 | }
60 | // 释放PID文件
61 | defer cntxt.Release()
62 | // 这里是子进程运行的代码
63 | logger.Info("- - - - - - - - - - - - - - -")
64 | logger.Info("daemon started")
65 | }
66 |
--------------------------------------------------------------------------------
/cmd/set_shutdown_hander.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "log"
6 | "os/signal"
7 | "syscall"
8 | "time"
9 |
10 | "github.com/yumenaka/comigo/assets/locale"
11 | "github.com/yumenaka/comigo/config"
12 | "github.com/yumenaka/comigo/model"
13 | "github.com/yumenaka/comigo/util/logger"
14 | )
15 |
16 | // SetShutdownHandler TODO:退出时清理临时文件的函数
17 | func SetShutdownHandler() {
18 | // 优雅地停止或重启: https://github.com/gin-gonic/examples/blob/master/graceful-shutdown/graceful-shutdown/notify-with-context/server.go
19 | // 创建侦听来自操作系统的中断信号的上下文。
20 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
21 | defer stop()
22 | // Listen for the interrupt signal.
23 | // 监听中断信号。
24 | <-ctx.Done()
25 | // 恢复中断信号的默认行为并通知用户关机。
26 | stop()
27 | log.Println(locale.GetString("shutdown_hint"))
28 | // 清理临时文件
29 | if config.GetClearCacheExit() {
30 | logger.Infof("\r"+locale.GetString("start_clear_file")+" CachePath:%s ", config.GetCachePath())
31 | model.ClearTempFilesALL(config.GetDebug(), config.GetCachePath())
32 | logger.Infof("%s", locale.GetString("clear_temp_file_completed"))
33 | }
34 | // 上下文用于通知服务器它有 5 秒的时间来完成它当前正在处理的请求
35 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
36 | defer cancel()
37 | // 只能通过http.Server.Shutdown()/http.Server.Close()等http包里的方法去实现,没办法自己实现.
38 | // 因为这样的设计即使你给自定义Server接口的实现类设计了Shutdown()方法,也调用不到.
39 | // 本质上还是因为从端口启动开始,后续的所有工作都是http包来完成的,我们无法干涉这其中的步骤
40 | if err := config.Server.Shutdown(ctx); err != nil {
41 | // logger.Infof("Comigo Server forced to shutdown: ", err)
42 | // time.Sleep(3 * time.Second)
43 | log.Fatal("Comigo Server forced to shutdown: ", err)
44 | }
45 | log.Println("Comigo Server exit.")
46 | }
47 |
--------------------------------------------------------------------------------
/cmd/set_store_path.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/yumenaka/comigo/config"
7 | "github.com/yumenaka/comigo/routers/upload_api"
8 | "github.com/yumenaka/comigo/util"
9 | "github.com/yumenaka/comigo/util/logger"
10 | )
11 |
12 | // SetStorePath 添加默认扫描路径 args[1:]是用户指定的扫描路径
13 | func SetStorePath(args []string) {
14 | // 如果用户指定了扫描路径,就把指定的路径都加入到扫描路径里面
15 | config.InitCfgStores()
16 | // 没指定扫描路径,配置文件也没设置书库文件夹的时候,默认把【当前工作目录】作为扫描路径
17 | if len(args) == 0 && len(config.GetLocalStoresList()) == 0 {
18 | // 获取当前工作目录
19 | wd, err := os.Getwd()
20 | if err != nil {
21 | logger.Infof("Failed to get working directory:%s", err)
22 | }
23 | logger.Infof("Working directory:%s", wd)
24 | config.AddLocalStore(wd)
25 | }
26 | // 指定了路径,就都扫描一遍
27 | for key, arg := range args {
28 | if config.GetDebug() {
29 | logger.Infof("args[%d]: %s\n", key, arg)
30 | }
31 | config.AddLocalStore(arg)
32 | }
33 | // 如果用户启用上传,且用户指定的上传路径不为空,就把上传路径也加入到扫描路径
34 | if config.GetEnableUpload() {
35 | if config.GetUploadPath() != "" {
36 | // 判断上传路径是否已经在扫描路径里面了
37 | for _, store := range config.GetLocalStoresList() {
38 | // 如果用户指定的上传路径,已经在扫描路径里面了,就不需要添加
39 | if store == config.GetUploadPath() {
40 | return
41 | }
42 | }
43 | // 把上传路径添加到扫描路径里面去
44 | config.AddLocalStore(config.GetUploadPath())
45 | }
46 | // 如果用户启用上传,但没有指定上传路径,就把【本地存储】里面的第一个路径作为上传路径
47 | if config.GetUploadPath() == "" {
48 | for _, store := range config.GetLocalStoresList() {
49 | if util.IsExist(store) {
50 | config.SetUploadPath(store)
51 | config.AddLocalStore(config.GetUploadPath())
52 | break
53 | }
54 | }
55 | }
56 | }
57 | // 把扫描路径设置,传递给handlers包
58 | upload_api.ConfigEnableUpload = &config.GetCfg().EnableUpload
59 | upload_api.ConfigUploadPath = &config.GetCfg().UploadPath
60 | }
61 |
--------------------------------------------------------------------------------
/cmd/set_stores.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/yumenaka/comigo/util/scan"
7 |
8 | "github.com/spf13/viper"
9 | "github.com/yumenaka/comigo/config"
10 | "github.com/yumenaka/comigo/internal/database"
11 | "github.com/yumenaka/comigo/model"
12 | "github.com/yumenaka/comigo/util/logger"
13 | )
14 |
15 | // ScanStore 解析命令,扫描文件,设置书库等
16 | func ScanStore(args []string) {
17 | // 1. 初始化数据库
18 | if config.GetEnableDatabase() {
19 | // 从数据库中读取书籍信息并持久化
20 | if err := database.InitDatabase(viper.ConfigFileUsed()); err != nil {
21 | logger.Infof("%s", err)
22 | }
23 | books, err := database.GetBooksFromDatabase()
24 | if err != nil {
25 | logger.Infof("%s", err)
26 | } else {
27 | model.RestoreDatabaseBooks(books)
28 | logger.Infof("从数据库中读取书籍信息,一共有 %d 本书", strconv.Itoa(len(books)))
29 | }
30 | }
31 | // 2、设置默认书库路径:扫描CMD指定的路径,或添加当前文件夹为默认路径。
32 | SetStorePath(args)
33 | // 3、扫描配置文件里面的书库路径
34 | err := scan.InitAllStore(scan.NewOption(config.GetCfg()))
35 | if err != nil {
36 | logger.Infof("Failed to scan store path: %v", err)
37 | }
38 | // 4、保存扫描结果到数据库
39 | if config.GetEnableDatabase() {
40 | err = scan.SaveResultsToDatabase(viper.ConfigFileUsed(), config.GetClearDatabaseWhenExit())
41 | if err != nil {
42 | logger.Infof("Failed SaveResultsToDatabase: %v", err)
43 | return
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/cmd/show_qrcode.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/sanity-io/litter"
7 | "github.com/yumenaka/comigo/assets/locale"
8 | "github.com/yumenaka/comigo/config"
9 | "github.com/yumenaka/comigo/model"
10 | "github.com/yumenaka/comigo/util"
11 | "github.com/yumenaka/comigo/util/logger"
12 | )
13 |
14 | func ShowQRCode() {
15 | // 如果只有一本书,URL 需要附加的参数
16 | etcStr := ""
17 | if model.GetBooksNumber() == 1 {
18 | bookList, err := model.GetAllBookInfoList("name")
19 | if err != nil {
20 | logger.Infof("Error getting book list: %s", err)
21 | return
22 | }
23 | if len(bookList.BookInfos) == 1 {
24 | etcStr = fmt.Sprintf("/#/%s/%s", config.GetDefaultMode(), bookList.BookInfos[0].BookID)
25 | }
26 | }
27 |
28 | enableTLS := config.GetCertFile() != "" && config.GetKeyFile() != ""
29 | outIP := config.GetHost()
30 | if config.GetHost() == "" {
31 | outIP = util.GetOutboundIP().String()
32 | }
33 |
34 | util.PrintAllReaderURL(
35 | config.GetPort(),
36 | config.GetOpenBrowser(),
37 | config.GetPrintAllPossibleQRCode(),
38 | outIP,
39 | config.GetDisableLAN(),
40 | enableTLS,
41 | etcStr,
42 | )
43 |
44 | // 打印配置,调试用
45 | if config.GetDebug() {
46 | litter.Dump(config.GetCfg())
47 | }
48 |
49 | fmt.Println(locale.GetString("ctrl_c_hint"))
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/tui/tool.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
--------------------------------------------------------------------------------
/cmd/wasm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Go+WASM ZIP 图片预览
6 |
10 |
11 |
12 | 选择 ZIP 文件:
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/config/init.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 | "runtime"
6 |
7 | "github.com/joho/godotenv"
8 | "github.com/yumenaka/comigo/util/logger"
9 | )
10 |
11 | // home目录 配置
12 | func init() {
13 | // 在非js环境下
14 | if runtime.GOOS == "js" {
15 | // Find home directory.
16 | home, err := os.UserHomeDir()
17 | if err != nil {
18 | logger.Infof("%s", err)
19 | }
20 | cfg.LogFilePath = home
21 | cfg.LogFileName = "comigo.log"
22 | }
23 | }
24 |
25 | // smb配置(TODO:SMB支持)
26 | func init() {
27 | err := godotenv.Load()
28 | if err != nil {
29 | if cfg.Debug {
30 | logger.Infof("Not found .env file")
31 | }
32 | }
33 | cfg.Stores[0].Smb.Host = os.Getenv("SMB_HOST")
34 | cfg.Stores[0].Smb.Username = os.Getenv("SMB_USER")
35 | cfg.Stores[0].Smb.Password = os.Getenv("SMB_PASS")
36 | cfg.Stores[0].Smb.ShareName = os.Getenv("SMB_SHARE_NAME")
37 | cfg.Stores[0].Smb.Path = os.Getenv("SMB_PATH")
38 | }
39 |
--------------------------------------------------------------------------------
/config/plugin.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | // FrpClientConfig frp客户端配置
4 | type FrpClientConfig struct {
5 | FrpcCommand string `comment:"手动设定frpc可执行程序的路径,默认为frpc"`
6 | ServerAddr string
7 | ServerPort int
8 | Token string
9 | FrpType string // 本地转发端口设置
10 | RemotePort int
11 | RandomRemotePort bool
12 | }
13 |
14 | // WebPServerConfig WebPServer服务端配置
15 | type WebPServerConfig struct {
16 | WebpCommand string
17 | HOST string
18 | PORT string
19 | ImgPath string
20 | QUALITY int
21 | AllowedTypes []string
22 | ExhaustPath string
23 | }
24 |
--------------------------------------------------------------------------------
/config/stores/store.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | type StoreType int
4 |
5 | const (
6 | Local StoreType = 1 + iota
7 | FTP
8 | SMB
9 | SFTP
10 | WebDAV
11 | S3
12 | )
13 |
14 | // Store 书库设置
15 | type Store struct {
16 | // 书库的类型,计划支持local,smb(2或3)ftp、sftp、webdav
17 | Type StoreType
18 | // 本地书库配置
19 | Local LocalOption
20 | // smb书库配置
21 | Smb SMBOption
22 | }
23 |
24 | type LocalOption struct {
25 | // 书库路径
26 | Path string
27 | }
28 | type SMBOption struct {
29 | // 书库的地址
30 | Host string
31 | // 书库的端口
32 | Port int
33 | // 书库的用户名
34 | Username string
35 | // 书库的密码
36 | Password string
37 | // smb的共享名
38 | ShareName string
39 | // 二级路径
40 | Path string
41 | }
42 |
--------------------------------------------------------------------------------
/config/version.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | var version = "v1.0.3"
4 |
5 | func GetVersion() string {
6 | return version
7 | }
8 |
--------------------------------------------------------------------------------
/goversioninfo.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/icon.ico
--------------------------------------------------------------------------------
/internal/ent/Readme.md:
--------------------------------------------------------------------------------
1 |
2 | ```bash
3 | go install entgo.io/ent/cmd/ent
4 | ent generate ./internal/ent/schema
5 | ```
6 |
7 |
8 |
9 | 来自facebook,官方简介:
10 | https://github.com/ent/ent/blob/master/README_zh.md
11 | 文档:
12 | https://entgo.io/zh/docs/tutorial-setup/
13 |
14 |
15 | Supported platforms and architectures
16 | https://pkg.go.dev/modernc.org/sqlite#hdr-Supported_platforms_and_architectures
17 | https://modern-c.appspot.com/-/builder/?importpath=modernc.org%2fsqlite
18 | 主要平台里面,也就windows 386不支持。
19 |
20 | Adds support for Go fs.FS based SQLite virtual filesystems, see function New in modernc.org/sqlite/vfs and/or TestVFS in all_test.go
21 | 添加对 Go fs 的支持。基于 FS 的 SQLite 虚拟文件系统,请参阅函数 modernc.org/sqlite/vfs 中的新功能和/或 all_test.go 中的 TestVFS
22 |
23 | https://gitlab.com/cznic/sqlite/-/blob/master/all_test.go
24 | 大约在2487行,搜索TestVFS,有示例代码。似乎可以内嵌数据库文件?可以存储默认配置,但无法保存的话,实际也没太大用处?
25 |
26 |
27 |
28 | ent是一个简单而又功能强大的Go语言实体框架,ent易于构建和维护应用程序与大数据模型。
29 | 图就是代码 - 将任何数据库表建模为Go对象。
30 | 轻松地遍历任何图形 - 可以轻松地运行查询、聚合和遍历任何图形结构。
31 | 静态类型和显式API - 使用代码生成静态类型和显式API,查询数据更加便捷。
32 | 多存储驱动程序 - 支持MySQL, PostgreSQL, SQLite 和 Gremlin。
33 | 可扩展 - 简单地扩展和使用Go模板自定义。
34 |
35 | 100%型安全なgolangORM「ent」を使ってみた
36 | https://future-architect.github.io/articles/20210728a/
37 |
38 | ```bash
39 | # 在项目根目录执行,生成设计图(Schema)模板。
40 | # 会生成 与对应的 schema/ent/book.go 与 schema/ent/user.go,编辑这些文件来定义实体的属性。
41 | #go run entgo.io/ent/cmd/ent init User Book
42 | 因为我的目录不在根目录下,所以应该指定路径
43 |
44 | go run entgo.io/ent/cmd/ent init --target /internal/ent/schema/User Book
45 |
46 | # 新建User与Book实体
47 | go run -mod=mod entgo.io/ent/cmd/ent User Book
48 |
49 | # 应该编辑 ent/schema/book.go 与 ent/schema/user.go。
50 | # 不应编辑生成的文件(ent/book.go 与 ent/user.go等等)。重新生成时,修改将消失。
51 | # 生成CRUD相关代码。每次添加或修改 fields 和 edges后, 都需要生成新的实体.
52 | # 在项目的根目录执行 ent generate或直接执行:
53 | go generate ./internal/ent
54 | ```
55 |
--------------------------------------------------------------------------------
/internal/ent/generate.go:
--------------------------------------------------------------------------------
1 | package ent
2 |
3 | //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
4 |
--------------------------------------------------------------------------------
/internal/ent/predicate/predicate.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package predicate
4 |
5 | import (
6 | "entgo.io/ent/dialect/sql"
7 | )
8 |
9 | // Book is the predicate function for book builders.
10 | type Book func(*sql.Selector)
11 |
12 | // SinglePageInfo is the predicate function for singlepageinfo builders.
13 | type SinglePageInfo func(*sql.Selector)
14 |
15 | // User is the predicate function for user builders.
16 | type User func(*sql.Selector)
17 |
--------------------------------------------------------------------------------
/internal/ent/runtime/runtime.go:
--------------------------------------------------------------------------------
1 | // Code generated by ent, DO NOT EDIT.
2 |
3 | package runtime
4 |
5 | // The schema-stitching logic is generated in github.com/yumenaka/comigo/internal/ent/runtime.go
6 |
7 | const (
8 | Version = "v0.14.3" // Version of ent codegen.
9 | Sum = "h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=" // Sum of ent codegen.
10 | )
11 |
--------------------------------------------------------------------------------
/internal/ent/schema/book.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/edge"
8 | "entgo.io/ent/schema/field"
9 | )
10 |
11 | // Book 定义书籍,BookID不应该重复,根据文件路径生成
12 | type Book struct {
13 | ent.Schema
14 | }
15 |
16 | // Fields 每次添加或修改 fields 和 edges后, 都需要在项目的根目录执行 go generate ./ent 命令重新生成文件
17 | // Fields of the Book.
18 | func (Book) Fields() []ent.Field {
19 | return []ent.Field{
20 | field.String("Title").
21 | MaxLen(1024). // 限制长度
22 | Comment("书名"),
23 | field.String("BookID").
24 | Unique().Comment("书籍ID"), // 字段可以使用 Unique 方法定义为唯一字段。 注意:唯一字段不能有默认值。
25 | field.String("Owner").
26 | Default("admin").
27 | Comment("拥有者"),
28 | field.String("FilePath").Comment("文件路径"),
29 | field.String("BookStorePath").Comment("书库路径"),
30 | field.String("Type").Comment("书籍类型"),
31 | field.Int("ChildBookNum").NonNegative(),
32 | field.Int("Depth").NonNegative(),
33 | field.String("ParentFolder"),
34 | field.Int("PageCount").
35 | NonNegative(). // 内置校验器,非负数
36 | Comment("总页数"),
37 | field.Int64("Size"),
38 | field.String("Authors"),
39 | field.String("ISBN"),
40 | field.String("Press"),
41 | field.String("PublishedAt"),
42 | field.String("ExtractPath"),
43 | field.Time("Modified").
44 | Default(time.Now). // 设置默认值
45 | Comment("创建时间"),
46 | field.Int("ExtractNum"),
47 | field.Bool("InitComplete"),
48 | field.Float("ReadPercent"),
49 | field.Bool("NonUTF8Zip"),
50 | field.String("ZipTextEncoding"),
51 | }
52 | }
53 |
54 | // Edges of the Book.
55 | func (Book) Edges() []ent.Edge {
56 | return []ent.Edge{
57 | edge.To("PageInfos", SinglePageInfo.Type), // Type是一种虚拟方法,用于Edge(关系)声明。
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/internal/ent/schema/singlepageinfo.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/field"
8 | )
9 |
10 | // SinglePageInfo holds the schema definition for the SinglePageInfo entity.
11 | type SinglePageInfo struct {
12 | ent.Schema
13 | }
14 |
15 | // Fields of the SinglePageInfo.
16 | func (SinglePageInfo) Fields() []ent.Field {
17 | return []ent.Field{
18 | field.String("BookID"),
19 | field.Int("PageNum"),
20 | field.String("Path"),
21 | field.String("Name"),
22 | field.String("Url"),
23 | field.String("BlurHash"),
24 | field.Int("Height"),
25 | field.Int("Width"),
26 | field.Time("ModTime").Default(time.Now),
27 | field.Int64("Size"),
28 | field.String("ImgType"),
29 | }
30 | }
31 |
32 | // Edges of the SinglePageInfo.
33 | func (SinglePageInfo) Edges() []ent.Edge {
34 | return nil
35 | // TODO: 如何在这里加上这个关系? https://entgo.io/zh/docs/tutorial-todo-crud
36 | //return []ent.Edge{
37 | // edge.From("BookID", Book.Type).
38 | // Ref("Pages").
39 | // Unique(),
40 | //}
41 | }
42 |
--------------------------------------------------------------------------------
/internal/ent/schema/user.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent"
7 | "entgo.io/ent/schema/field"
8 | )
9 |
10 | // User holds the schema definition for the User entity.
11 | type User struct {
12 | ent.Schema
13 | }
14 |
15 | // Fields of the User.
16 | func (User) Fields() []ent.Field {
17 | return []ent.Field{
18 | field.String("name").
19 | MaxLen(50). // 限制长度
20 | Unique().Comment("用户称呼"),
21 | field.Time("created_at").
22 | Default(time.Now).Comment("创建时间"),
23 | field.String("username").Comment("用户名").
24 | MaxLen(50). // 限制长度
25 | Unique(), // 字段可以使用 Unique 方法定义为唯一字段。 注意:唯一字段不能有默认值。
26 | field.String("password").Comment("登录密码"),
27 | field.Time("last_login").
28 | Default(time.Now).Comment("最后登录时间"),
29 | field.Int("age").
30 | Positive(), // 只能取正数
31 | }
32 | }
33 |
34 | // Edges of the User.
35 | func (User) Edges() []ent.Edge {
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | //go:build !js
2 |
3 | //go:generate go install -v github.com/josephspurrier/goversioninfo/cmd/goversioninfo
4 | //go:generate goversioninfo -icon=icon.ico -manifest=goversioninfo.exe.manifest
5 | package main
6 |
7 | import (
8 | "os"
9 |
10 | tea "github.com/charmbracelet/bubbletea"
11 | "github.com/charmbracelet/x/term"
12 | "github.com/yumenaka/comigo/cmd"
13 | "github.com/yumenaka/comigo/cmd/tui"
14 | "github.com/yumenaka/comigo/routers"
15 | "github.com/yumenaka/comigo/util/logger"
16 | )
17 |
18 | // 运行 Comigo 服务器
19 | func main() {
20 | // 初始化命令行flag与args,环境变量与配置文件
21 | cmd.Execute()
22 | // 启动网页服务器(不阻塞)
23 | routers.StartWebServer()
24 | // 扫描书库(命令行指定)
25 | cmd.ScanStore(cmd.Args)
26 | // 在命令行显示QRCode
27 | cmd.ShowQRCode()
28 | // 退出时清理临时文件的处理函数
29 | cmd.SetShutdownHandler()
30 |
31 | // RunTui()
32 | }
33 |
34 | // RunTui tui实验
35 | func RunTui() {
36 | // 判断是否在终端中运行
37 | if term.IsTerminal(os.Stdout.Fd()) {
38 | // 1. 初始化自定义的日志缓冲区
39 | logBuffer := tui.NewLogBuffer()
40 | // 将标准日志的输出重定向到 logBuffer
41 | logger.SetOutput(logBuffer)
42 |
43 | // 2. 创建 Bubble Tea 程序的模型
44 | model := tui.InitialModel(logBuffer)
45 | // 创建一个bubbletea的应用对象
46 | program := tea.NewProgram(model)
47 |
48 | // Comigo 服务器的初始化(初始化 Comigo 命令行flag与args,环境变量与配置文件)
49 | cmd.Execute()
50 | // 启动网页服务器(不阻塞)
51 | routers.StartWebServer()
52 | // 扫描书库(命令行指定)
53 | cmd.ScanStore(cmd.Args)
54 |
55 | // 3. 调用 Bubble Tea 对象的Start()方法开始执行,运行 TUI 程序
56 | if _, err := program.Run(); err != nil {
57 | logger.Errorf("Error running tui interface: %v", err)
58 | }
59 | } else {
60 | // 初始化命令行flag与args,环境变量与配置文件
61 | cmd.Execute()
62 | // 启动网页服务器(不阻塞)
63 | routers.StartWebServer()
64 | // 扫描书库(命令行指定)
65 | cmd.ScanStore(cmd.Args)
66 | // 在命令行显示QRCode
67 | cmd.ShowQRCode()
68 | // 退出时清理临时文件的处理函数
69 | cmd.SetShutdownHandler()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/model/book_group.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | type BookGroup struct {
9 | BookInfo
10 | ChildBook sync.Map // key:BookID,value: *BookInfo
11 | }
12 |
13 | // NewBookGroup 初始化BookGroup,设置文件路径、书名、BookID等等
14 | func NewBookGroup(filePath string, modified time.Time, fileSize int64, storePath string, depth int, bookType SupportFileType) (*BookGroup, error) {
15 | // 初始化书籍
16 | group := BookGroup{
17 | BookInfo: BookInfo{
18 | Modified: modified,
19 | FileSize: fileSize,
20 | InitComplete: false,
21 | Depth: depth,
22 | BookStorePath: storePath,
23 | Type: bookType,
24 | },
25 | }
26 | // 设置属性:
27 | group.setTitle(filePath).setFilePath(filePath).setAuthor().setParentFolder(filePath).initBookID()
28 | return &group, nil
29 | }
30 |
--------------------------------------------------------------------------------
/model/book_util.go:
--------------------------------------------------------------------------------
1 | //go:build !js
2 |
3 | package model
4 |
5 | import (
6 | "github.com/cheggaaa/pb/v3"
7 | "github.com/xxjwxc/gowp/workpool"
8 | "github.com/yumenaka/comigo/assets/locale"
9 | "github.com/yumenaka/comigo/util/logger"
10 | )
11 |
12 | // ScanAllImage 服务器端分析分辨率、漫画单双页,只适合已解压文件
13 | func (b *Book) ScanAllImage() {
14 | logger.Infof(locale.GetString("check_image_start"))
15 | bar := pb.StartNew(b.GetPageCount())
16 | for i := range b.Pages.Images {
17 | analyzePageImages(&b.Pages.Images[i], b.FilePath)
18 | bar.Increment()
19 | }
20 | bar.Finish()
21 | logger.Infof(locale.GetString("check_image_completed"))
22 | }
23 |
24 | // ScanAllImageGo 并发分析图片
25 | func (b *Book) ScanAllImageGo() {
26 | logger.Infof(locale.GetString("check_image_start"))
27 | wp := workpool.New(10) // 设置最大线程数
28 | bar := pb.StartNew(b.GetPageCount())
29 |
30 | for i := range b.Pages.Images {
31 | i := i // 避免闭包问题
32 | wp.Do(func() error {
33 | analyzePageImages(&b.Pages.Images[i], b.FilePath)
34 | bar.Increment()
35 | return nil
36 | })
37 | }
38 | _ = wp.Wait()
39 | bar.Finish()
40 | logger.Infof(locale.GetString("check_image_completed"))
41 | }
42 |
--------------------------------------------------------------------------------
/model/dir_node.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // DirNode 表示目录树节点,用于 JSON 存储模式
4 | type DirNode struct {
5 | Name string `json:"name"`
6 | Path string `json:"path"`
7 | SubDirs []DirNode `json:"sub_dirs"` // 子目录列表
8 | Files []MediaFileInfo `json:"files"` // 本目录下的图片文件列表
9 | }
10 |
--------------------------------------------------------------------------------
/model/support_file_type.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "path"
5 | "strings"
6 | )
7 |
8 | type SupportFileType string
9 |
10 | // 书籍类型
11 | const (
12 | TypeDir SupportFileType = "dir"
13 | TypeZip SupportFileType = ".zip"
14 | TypeRar SupportFileType = ".rar"
15 | TypeBooksGroup SupportFileType = "book_group"
16 | TypeCbz SupportFileType = ".cbz"
17 | TypeCbr SupportFileType = ".cbr"
18 | TypeTar SupportFileType = ".tar"
19 | TypeEpub SupportFileType = ".epub"
20 | TypePDF SupportFileType = ".pdf"
21 | TypeVideo SupportFileType = "video"
22 | TypeAudio SupportFileType = "audio"
23 | TypeUnknownFile SupportFileType = "unknown"
24 | )
25 |
26 | // GetBookTypeByFilename 初始化Book时,取得BookType
27 | func GetBookTypeByFilename(filename string) SupportFileType {
28 | // 获取文件后缀
29 | switch strings.ToLower(path.Ext(filename)) {
30 | case ".zip":
31 | return TypeZip
32 | case ".rar":
33 | return TypeRar
34 | case ".cbz":
35 | return TypeCbz
36 | case ".cbr":
37 | return TypeCbr
38 | case ".epub":
39 | return TypeEpub
40 | case ".tar":
41 | return TypeTar
42 | case ".pdf":
43 | return TypePDF
44 | case ".mp4", ".m4v", ".flv", ".avi", ".webm":
45 | return TypeVideo
46 | case ".mp3", ".wav", ".wma", ".ogg":
47 | return TypeAudio
48 | default:
49 | return TypeUnknownFile
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/model/tool.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | type ReadingProgress struct {
8 | // 当前页
9 | NowPageNum int `json:"nowPageNum"`
10 | // 当前章节
11 | NowChapterNum int `json:"nowChapterNum"`
12 | // 阅读时间,单位为秒
13 | ReadingTime int `json:"readingTime"`
14 | }
15 |
16 | func GetReadingProgress(progress string) (ReadingProgress, error) {
17 | // 创建一个ReadingProgress实例用于保存解析结果
18 | var rp ReadingProgress
19 | // 将JSON字符串解析到ReadingProgress结构体中
20 | err := json.Unmarshal([]byte(progress), &rp)
21 | if err != nil {
22 | // 如果解析出错,返回空的ReadingProgress和错误信息
23 | return ReadingProgress{}, err
24 | }
25 | // 返回解析后的ReadingProgress和nil错误
26 | return rp, nil
27 | }
28 |
--------------------------------------------------------------------------------
/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type User struct {
4 | ID int `json:"id"`
5 | Username string `json:"username"`
6 | Password string `json:"password"`
7 | FirstName string `json:"first_name"`
8 | LastName string `json:"last_name"`
9 | Email string `json:"email"`
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "comigo",
3 | "version": "1.0.0",
4 | "description": "Frontend for Comigo.",
5 | "license": "MIT",
6 | "browserslist": "> 0.5%, last 2 versions, not dead",
7 | "scripts": {
8 | "fmt": "prettier --write .",
9 | "build": "parcel build ./assets/main.js ./assets/styles.css --dist-dir assets/script",
10 | "dev": "parcel build ./assets/main.js ./assets/styles.css --dist-dir assets/script --no-optimize",
11 | "watch": "parcel watch ./assets/main.js ./assets/styles.css --dist-dir assets/script --no-optimize"
12 | },
13 | "dependencies": {
14 | "@alpinejs/morph": "latest",
15 | "@alpinejs/persist": "latest",
16 | "alpinejs": "latest",
17 | "flowbite": "latest",
18 | "htmx.org": "latest",
19 | "i18next": "latest",
20 | "i18next-browser-languagedetector": "latest",
21 | "screenfull": "latest",
22 | "tailwindcss": "latest"
23 | },
24 | "devDependencies": {
25 | "@tailwindcss/forms": "latest",
26 | "@tailwindcss/typography": "latest",
27 | "@tailwindcss/postcss": "latest",
28 | "@parcel/transformer-css": "latest",
29 | "parcel": "latest",
30 | "postcss": "latest",
31 | "prettier": "latest"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/routers/config_api/delete_config.go:
--------------------------------------------------------------------------------
1 | package config_api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/yumenaka/comigo/config"
8 | "github.com/yumenaka/comigo/util/logger"
9 | )
10 |
11 | const (
12 | HomeDirectory = "HomeDirectory"
13 | WorkingDirectory = "WorkingDirectory"
14 | ProgramDirectory = "ProgramDirectory"
15 | )
16 |
17 | // DeleteConfig 删除配置文件
18 | func DeleteConfig(c echo.Context) error {
19 | in := c.Param("in")
20 | validDirs := []string{WorkingDirectory, HomeDirectory, ProgramDirectory}
21 |
22 | if !contains(validDirs, in) {
23 | logger.Infof("error: Failed to delete config in %s directory", in)
24 | return c.JSON(http.StatusBadRequest, map[string]string{
25 | "error": "Failed to delete config in " + in + " directory",
26 | })
27 | }
28 |
29 | err := config.DeleteConfigIn(in)
30 | if err != nil {
31 | return c.JSON(http.StatusMethodNotAllowed, map[string]string{
32 | "error": "Failed to delete config",
33 | })
34 | }
35 |
36 | return GetConfigStatus(c)
37 | }
38 |
39 | // contains 检查切片是否包含特定字符串
40 | func contains(slice []string, str string) bool {
41 | for _, v := range slice {
42 | if v == str {
43 | return true
44 | }
45 | }
46 | return false
47 | }
48 |
--------------------------------------------------------------------------------
/routers/config_api/get_config.go:
--------------------------------------------------------------------------------
1 | package config_api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/pelletier/go-toml/v2"
8 | "github.com/yumenaka/comigo/config"
9 | "github.com/yumenaka/comigo/util/logger"
10 | )
11 |
12 | // GetConfig 获取json格式的当前配置,不做修改
13 | func GetConfig(c echo.Context) error {
14 | return c.JSON(http.StatusOK, config.GetCfg())
15 | }
16 |
17 | // GetConfigToml 下载服务器配置(toml),修改关键值后上传
18 | func GetConfigToml(c echo.Context) error {
19 | // golang结构体默认深拷贝(但是基本类型浅拷贝)
20 | tempConfig := config.GetCfg()
21 | tempConfig.LogFilePath = ""
22 | tempConfig.OpenBrowser = false
23 | tempConfig.EnableDatabase = true
24 | tempConfig.LocalStores = []string{"C:\\test\\Comic", "D:\\some_path\\book", "/home/user/download"}
25 | tempConfig.Username = "You_can_change_this_username"
26 | tempConfig.Password = "Some_Secret-.PasswordNot_guessable"
27 |
28 | bytes, err := toml.Marshal(tempConfig)
29 | if err != nil {
30 | logger.Infof("%s", "toml.Marshal Error")
31 | return err
32 | }
33 |
34 | // 在命令行打印
35 | logger.Infof("%s", string(bytes))
36 |
37 | // 设置响应头,指定文件下载名称和类型
38 | return c.Blob(
39 | http.StatusOK,
40 | "application/octet-stream",
41 | bytes,
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/routers/config_api/get_config_status.go:
--------------------------------------------------------------------------------
1 | package config_api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/yumenaka/comigo/config"
8 | )
9 |
10 | // GetConfigStatus 获取json格式的当前配置
11 | func GetConfigStatus(c echo.Context) error {
12 | err := config.CfgStatus.SetConfigStatus()
13 | if err != nil {
14 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get config"})
15 | }
16 | return c.JSON(http.StatusOK, config.CfgStatus)
17 | }
18 |
--------------------------------------------------------------------------------
/routers/get_data_api/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/routers/get_data_api/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/routers/get_data_api/get_book.go:
--------------------------------------------------------------------------------
1 | package get_data_api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/yumenaka/comigo/model"
8 | "github.com/yumenaka/comigo/util/file"
9 | "github.com/yumenaka/comigo/util/logger"
10 | )
11 |
12 | // GetBook 相关参数:
13 | // id:书籍的ID,必须项目 &id=2b17a130
14 | // author:书籍的作者,未必存在 &author=佚名
15 | // sort_page:按照自然文件名重新排序 &sort_page=true
16 | // 示例 URL: http://127.0.0.1:1234/api/get_book?id=1215a&sort_by=name
17 | // 示例 URL: http://127.0.0.1:1234/api/get_book?&author=Doe&name=book_name
18 | func GetBook(c echo.Context) error {
19 | author := c.QueryParam("author")
20 | sortBy := c.QueryParam("sort_by")
21 | if sortBy == "" {
22 | sortBy = "default"
23 | }
24 | id := c.QueryParam("id")
25 |
26 | model.CheckAllBookFileExist()
27 |
28 | if author != "" {
29 | bookList, err := model.GetBookByAuthor(author, sortBy)
30 | if err != nil {
31 | logger.Infof("%s", err)
32 | }
33 | return c.JSON(http.StatusOK, bookList)
34 | }
35 |
36 | if id != "" {
37 | b, err := model.GetBookByID(id, sortBy)
38 | if err != nil {
39 | logger.Infof("%s", err)
40 | return c.JSON(http.StatusBadRequest, "id not found")
41 | }
42 |
43 | // 如果是epub文件,重新按照Epub信息排序
44 | if b.Type == model.TypeEpub && sortBy == "epub_info" {
45 | imageList, err := file.GetImageListFromEpubFile(b.FilePath)
46 | if err != nil {
47 | logger.Infof("%s", err)
48 | return c.JSON(http.StatusOK, b)
49 | }
50 | b.SortPagesByImageList(imageList)
51 | }
52 | return c.JSON(http.StatusOK, b)
53 | }
54 |
55 | return c.JSON(http.StatusBadRequest, "no valid parameters provided")
56 | }
57 |
--------------------------------------------------------------------------------
/routers/get_data_api/get_qrcode.go:
--------------------------------------------------------------------------------
1 | package get_data_api
2 |
3 | import (
4 | "encoding/base64"
5 | "net/http"
6 |
7 | "github.com/labstack/echo/v4"
8 | "github.com/skip2/go-qrcode"
9 | "github.com/yumenaka/comigo/config"
10 | "github.com/yumenaka/comigo/util/logger"
11 | )
12 |
13 | // GetQrcode 下载服务器配置
14 | func GetQrcode(c echo.Context) error {
15 | // 通过参数传递自定义文本,生成二维码
16 | qrcodeStr := c.QueryParam("qrcode_str")
17 | base64Encode := getBoolQueryParam(c, "base64", false)
18 | if qrcodeStr != "" {
19 | png, err := qrcode.Encode(qrcodeStr, qrcode.Medium, 256)
20 | if err != nil {
21 | logger.Infof("%s", err)
22 | return err
23 | }
24 | if base64Encode {
25 | // 返回Base64编码的二维码
26 | base64Str := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
27 | return c.String(http.StatusOK, base64Str)
28 | }
29 | return c.Blob(http.StatusOK, "image/png", png)
30 | }
31 | // 根据配置文件中的URL,生成二维码
32 | qrcodeStr = config.GetQrcodeURL()
33 | png, err := qrcode.Encode(qrcodeStr, qrcode.Medium, 256)
34 | if err != nil {
35 | logger.Infof("%s", err)
36 | return err
37 | }
38 | if base64Encode {
39 | // 返回Base64编码的二维码
40 | base64Str := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
41 | return c.String(http.StatusOK, base64Str)
42 | }
43 | return c.Blob(http.StatusOK, "image/png", png)
44 | }
45 |
--------------------------------------------------------------------------------
/routers/get_data_api/get_raw_file.go:
--------------------------------------------------------------------------------
1 | package get_data_api
2 |
3 | import (
4 | "net/http"
5 | "os"
6 |
7 | "github.com/labstack/echo/v4"
8 | "github.com/yumenaka/comigo/model"
9 | "github.com/yumenaka/comigo/util/logger"
10 | )
11 |
12 | func GetRawFile(c echo.Context) error {
13 | bookID := c.Param("book_id")
14 | b, err := model.GetBookByID(bookID, "")
15 | // 打印文件名
16 | if err != nil {
17 | return c.String(http.StatusNotFound, "404 page not found")
18 | }
19 | fileName := c.Param("file_name")
20 | logger.Infof("下载文件:%s", fileName)
21 |
22 | // 获取文件信息
23 | fileInfo, err := os.Stat(b.FilePath)
24 | if err != nil {
25 | return c.String(http.StatusNotFound, "404 page not found")
26 | }
27 | // 如果是目录,返回目录列表
28 | if fileInfo.IsDir() {
29 | return c.String(http.StatusNotFound, "404 page not found")
30 | }
31 | // 如果是文件,返回文件
32 | return c.File(b.FilePath)
33 | }
34 |
--------------------------------------------------------------------------------
/routers/get_data_api/get_reg_file.go:
--------------------------------------------------------------------------------
1 | package get_data_api
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/labstack/echo/v4"
11 | "github.com/yumenaka/comigo/util/logger"
12 | )
13 |
14 | // TODO:GetRegFile 设置注册表文件
15 | func GetRegFile(c echo.Context) error {
16 | // 获取当前可执行文件的路径
17 | exePath, err := os.Executable()
18 | if err != nil {
19 | logger.Infof("%s", err)
20 | return c.String(http.StatusInternalServerError, "Error getting executable path")
21 | }
22 |
23 | // 获取当前可执行文件的目录
24 | exeDir := filepath.Dir(exePath)
25 | // 获取当前可执行文件的名称(不含扩展名)
26 | exeName := strings.TrimSuffix(filepath.Base(exePath), filepath.Ext(exePath))
27 |
28 | // 构建注册表文件内容
29 | regContent := fmt.Sprintf(`Windows Registry Editor Version 5.00
30 |
31 | [HKEY_CLASSES_ROOT\comigo]
32 | @="URL:comigo Protocol"
33 | "URL Protocol"=""
34 |
35 | [HKEY_CLASSES_ROOT\comigo\DefaultIcon]
36 | @="\"%s\""
37 |
38 | [HKEY_CLASSES_ROOT\comigo\shell]
39 |
40 | [HKEY_CLASSES_ROOT\comigo\shell\open]
41 |
42 | [HKEY_CLASSES_ROOT\comigo\shell\open\command]
43 | @="\"%s\" \"%%1\""`, exePath, exePath)
44 |
45 | // 构建注册表文件路径
46 | regFilePath := filepath.Join(exeDir, exeName+".reg")
47 |
48 | // 写入注册表文件
49 | err = os.WriteFile(regFilePath, []byte(regContent), 0o644)
50 | if err != nil {
51 | logger.Infof("%s", err)
52 | return c.String(http.StatusInternalServerError, "Error writing reg file")
53 | }
54 |
55 | // 设置响应头
56 | c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.reg", exeName))
57 | c.Response().Header().Set("Content-Type", "application/x-windows-registry-script")
58 |
59 | // 返回文件
60 | return c.File(regFilePath)
61 | }
62 |
--------------------------------------------------------------------------------
/routers/get_data_api/get_status.go:
--------------------------------------------------------------------------------
1 | package get_data_api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/yumenaka/comigo/config"
8 | "github.com/yumenaka/comigo/model"
9 | "github.com/yumenaka/comigo/util"
10 | )
11 |
12 | // ServerStatus 服务器当前状况
13 | type ServerStatus struct {
14 | ServerName string // 服务器描述
15 | ServerHost string // 服务器地址
16 | ServerPort int // 服务器端口(程序运行的端口)
17 | NumberOfBooks int // 当前拥有的书籍总数
18 | NumberOfOnLineUser int // TODO:在线用户数
19 | NumberOfOnLineDevices int // TODO:在线设备数
20 | SupportUploadFile bool // 是否支持上传文件
21 | ClientIP string // 客户端IP
22 | OSInfo util.SystemStatus // 系统信息
23 | }
24 |
25 | func GetServerInfoHandler(c echo.Context) error {
26 | serverStatus := util.GetServerInfo(config.GetHost(), config.GetVersion(), config.GetPort(), config.GetEnableUpload(), model.GetBooksNumber())
27 | return c.JSON(http.StatusOK, serverStatus)
28 | }
29 |
30 | func GetAllServerInfoHandler(c echo.Context) error {
31 | serverStatus := util.GetAllServerInfo(config.GetHost(), config.GetVersion(), config.GetPort(), config.GetEnableUpload(), model.GetBooksNumber(), c.RealIP())
32 | return c.JSON(http.StatusOK, serverStatus)
33 | }
34 |
--------------------------------------------------------------------------------
/routers/set_cache.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | // noCache 中间件设置 HTTP 响应头,禁用缓存。
11 | // 这将确保每次请求都从服务器获取最新的响应,而不是使用缓存的版本。
12 | // 使用 noCache 中间件,会导强制浏览器每次都重新加载页面。与翻页模式的预加载功能冲突。所以除了测试和调试外,一般不启用。
13 | func noCache() echo.MiddlewareFunc {
14 | return func(next echo.HandlerFunc) echo.HandlerFunc {
15 | return func(c echo.Context) error {
16 | c.Response().Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
17 | c.Response().Header().Set("Pragma", "no-cache")
18 | c.Response().Header().Set("Expires", "0")
19 | c.Response().Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
20 | return next(c)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/routers/set_log.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/config"
6 | "github.com/yumenaka/comigo/util/logger"
7 | )
8 |
9 | // SetLogger 设置日志中间件
10 | func SetEchoLogger(e *echo.Echo) {
11 | // 设置log中间件
12 | logger.ReportCaller = config.GetDebug()
13 | // TODO:输出到tui
14 | echoLogHandler := logger.EchoLogHandler(config.GetLogToFile(), config.GetLogFilePath(), config.GetLogFileName(), config.GetDebug())
15 | e.Use(echoLogHandler)
16 | // // 设置日志级别
17 | // if config.GetDebug() {
18 | // e.Logger.SetLevel(log.DEBUG)
19 | // } else {
20 | // e.Logger.SetLevel(log.INFO)
21 | // }
22 | //
23 | // // 如果需要输出到文件
24 | // if config.GetLogToFile() {
25 | // // 确保日志目录存在
26 | // logDir := config.GetLogFilePath()
27 | // if err := os.MkdirAll(logDir, 0o755); err != nil {
28 | // logger.Errorf("创建日志目录失败: %v", err)
29 | // return
30 | // }
31 | //
32 | // // 打开日志文件
33 | // logFile := filepath.Join(logDir, config.GetLogFileName())
34 | // f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
35 | // if err != nil {
36 | // logger.Errorf("打开日志文件失败: %v", err)
37 | // return
38 | // }
39 | //
40 | // // 如果是调试模式,同时输出到文件和控制台
41 | // if config.GetDebug() {
42 | // e.Logger.SetOutput(io.MultiWriter(f, os.Stdout))
43 | // } else {
44 | // e.Logger.SetOutput(f)
45 | // }
46 | // } else {
47 | // // 如果不输出到文件,则输出到标准输出
48 | // e.Logger.SetOutput(os.Stdout)
49 | // }
50 | //
51 | // // 设置日志格式
52 | // e.Logger.SetHeader("${time_rfc3339} ${level} ${short_file}:${line} ${message}")
53 | }
54 |
--------------------------------------------------------------------------------
/routers/set_port.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import (
4 | "math/rand"
5 | "time"
6 |
7 | "github.com/yumenaka/comigo/assets/locale"
8 | "github.com/yumenaka/comigo/config"
9 | "github.com/yumenaka/comigo/util"
10 | "github.com/yumenaka/comigo/util/logger"
11 | )
12 |
13 | // SetHttpPort 设置服务端口
14 | func SetHttpPort() {
15 | // 检测端口是否可用
16 | if !util.CheckPort(config.GetPort()) {
17 | // 端口被占用
18 | logger.Infof(locale.GetString("port_busy"), config.GetPort())
19 | // 获取一个空闲的系统端口号
20 | port, err := util.GetFreePort()
21 | if err != nil {
22 | logger.Infof("Failed to get a free port: %v", err)
23 | // 如果无法获取空闲端口,则随机选择一个端口
24 | rand.New(rand.NewSource(time.Now().UnixNano()))
25 | if config.GetPort()+2000 > 65535 {
26 | config.SetPort(rand.Intn(1024) + config.GetPort())
27 | } else {
28 | config.SetPort(rand.Intn(20000) + 30000)
29 | }
30 | } else {
31 | config.SetPort(port)
32 | }
33 | logger.Infof("Using port: %d", config.GetPort())
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | // 监视这些文件的变化,然后编译CSS
4 | content: [
5 | './**/*.{html,templ}',
6 | '!./node_modules/**/*',
7 | './router/script/scripts.js',
8 | ],
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [
13 | require('@tailwindcss/forms'),
14 | require('@tailwindcss/typography'),
15 | require('flowbite/plugin')
16 | ],
17 | }
18 |
--------------------------------------------------------------------------------
/templ/common/footer.templ:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | templ Footer(version string) {
4 |
11 | }
12 |
--------------------------------------------------------------------------------
/templ/common/html.templ:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/assets"
6 | )
7 |
8 | // Html 定义网页布局
9 | templ Html(c echo.Context, bodyContent templ.Component, insertScript []string) {
10 |
11 |
12 |
13 |
14 |
15 |
16 | { GetPageTitle(c.Param("id")) }
17 |
18 |
19 |
20 |
21 |
22 |
23 | @templ.Raw(assets.GetCSS(c.QueryParam("static") != ""))
24 |
25 |
26 |
27 |
28 |
29 |
33 | @MessageModal()
34 | @bodyContent
35 |
36 |
37 | @templ.Raw(assets.GetJavaScript(c.QueryParam("static") != "", insertScript))
38 |
39 |
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/templ/common/qrcode.templ:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | templ QRCode(serverHost string) {
4 |
5 |
22 | }
23 |
--------------------------------------------------------------------------------
/templ/common/svg/arror_down.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ ArrowDown() {
4 |
7 | }
8 |
--------------------------------------------------------------------------------
/templ/common/svg/arror_down_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.887
4 | package svg
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | func ArrowDown() templ.Component {
12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
14 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
15 | return templ_7745c5c3_CtxErr
16 | }
17 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
18 | if !templ_7745c5c3_IsBuffer {
19 | defer func() {
20 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
21 | if templ_7745c5c3_Err == nil {
22 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
23 | }
24 | }()
25 | }
26 | ctx = templ.InitializeContext(ctx)
27 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
28 | if templ_7745c5c3_Var1 == nil {
29 | templ_7745c5c3_Var1 = templ.NopComponent
30 | }
31 | ctx = templ.ClearChildren(ctx)
32 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
33 | if templ_7745c5c3_Err != nil {
34 | return templ_7745c5c3_Err
35 | }
36 | return nil
37 | })
38 | }
39 |
40 | var _ = templruntime.GeneratedTemplate
41 |
--------------------------------------------------------------------------------
/templ/common/svg/book.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | // https://heroicons.com/
4 | templ Book() {
5 |
8 | }
9 |
--------------------------------------------------------------------------------
/templ/common/svg/close.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ Close() {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/templ/common/svg/delete.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ Delete() {
4 |
12 | }
--------------------------------------------------------------------------------
/templ/common/svg/fullscreen.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ FullScreen() {
4 |
20 | }
21 |
--------------------------------------------------------------------------------
/templ/common/svg/labs.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | // https://heroicons.com/
4 | templ Labs() {
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/templ/common/svg/labs_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.887
4 | package svg
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | // https://heroicons.com/
12 | func Labs() templ.Component {
13 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
14 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
15 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
16 | return templ_7745c5c3_CtxErr
17 | }
18 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
19 | if !templ_7745c5c3_IsBuffer {
20 | defer func() {
21 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
22 | if templ_7745c5c3_Err == nil {
23 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
24 | }
25 | }()
26 | }
27 | ctx = templ.InitializeContext(ctx)
28 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
29 | if templ_7745c5c3_Var1 == nil {
30 | templ_7745c5c3_Var1 = templ.NopComponent
31 | }
32 | ctx = templ.ClearChildren(ctx)
33 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
34 | if templ_7745c5c3_Err != nil {
35 | return templ_7745c5c3_Err
36 | }
37 | return nil
38 | })
39 | }
40 |
41 | var _ = templruntime.GeneratedTemplate
42 |
--------------------------------------------------------------------------------
/templ/common/svg/network.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | // https://heroicons.com/
4 | templ Network() {
5 |
8 | }
9 |
--------------------------------------------------------------------------------
/templ/common/svg/qrcode.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ QRCode() {
4 |
21 | }
22 |
--------------------------------------------------------------------------------
/templ/common/svg/return.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ Return() {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/templ/common/svg/return_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.887
4 | package svg
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | func Return() templ.Component {
12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
14 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
15 | return templ_7745c5c3_CtxErr
16 | }
17 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
18 | if !templ_7745c5c3_IsBuffer {
19 | defer func() {
20 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
21 | if templ_7745c5c3_Err == nil {
22 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
23 | }
24 | }()
25 | }
26 | ctx = templ.InitializeContext(ctx)
27 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
28 | if templ_7745c5c3_Var1 == nil {
29 | templ_7745c5c3_Var1 = templ.NopComponent
30 | }
31 | ctx = templ.ClearChildren(ctx)
32 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
33 | if templ_7745c5c3_Err != nil {
34 | return templ_7745c5c3_Err
35 | }
36 | return nil
37 | })
38 | }
39 |
40 | var _ = templruntime.GeneratedTemplate
41 |
--------------------------------------------------------------------------------
/templ/common/svg/server_disk.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ ServerDisk() {
4 |
18 | }
19 |
--------------------------------------------------------------------------------
/templ/common/svg/setting.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ Setting() {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/templ/common/svg/upload.templ:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | templ Upload() {
4 |
30 | }
31 |
--------------------------------------------------------------------------------
/templ/pages/error_page/not_found.go:
--------------------------------------------------------------------------------
1 | package error_page
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/angelofallars/htmx-go"
7 | "github.com/labstack/echo/v4"
8 | "github.com/yumenaka/comigo/templ/common"
9 | )
10 |
11 | // NotFoundCommon 共通的 404 页面
12 | func NotFoundCommon(c echo.Context) error {
13 | // 没有找到书,显示 HTTP 404 错误
14 | indexHtml := common.Html(
15 | c,
16 | NotFound404(c),
17 | []string{},
18 | )
19 | // 渲染 404 页面
20 | if err := htmx.NewResponse().RenderTempl(c.Request().Context(), c.Response().Writer, indexHtml); err != nil {
21 | // 渲染失败,返回 HTTP 500 错误。
22 | return c.NoContent(http.StatusInternalServerError)
23 | }
24 | return nil
25 | }
26 |
--------------------------------------------------------------------------------
/templ/pages/error_page/not_found.templ:
--------------------------------------------------------------------------------
1 | package error_page
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/templ/state"
6 | "github.com/yumenaka/comigo/templ/common"
7 | )
8 | // 404 NotFound页面
9 | templ NotFound404(c echo.Context) {
10 | @common.Header(
11 | common.HeaderProps{
12 | Title: "",
13 | ShowReturnIcon: true,
14 | ReturnUrl: "/",
15 | SetDownLoadLink: false,
16 | InShelf: false,
17 | DownLoadLink: "",
18 | SetTheme: true,
19 | })
20 |
21 |
25 |
26 |
404
27 |
Not Found
28 |
29 | @common.Drawer(c, nil, nil)
30 | @common.QRCode(state.ServerStatus.ServerHost)
31 | @common.Footer(state.Version)
32 | }
33 |
--------------------------------------------------------------------------------
/templ/pages/flip/flip.templ:
--------------------------------------------------------------------------------
1 | package flip
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/model"
6 | "github.com/yumenaka/comigo/templ/common"
7 | "github.com/yumenaka/comigo/templ/state"
8 | )
9 |
10 | // FlipPage 定义 BodyHTML
11 | templ FlipPage(c echo.Context, book *model.Book) {
12 | @common.Toast()
13 | @InsertData(book, state.ServerStatus)
14 | if book != nil {
15 | @common.Header(
16 | common.HeaderProps{
17 | Title: common.GetPageTitle(book.BookInfo.BookID),
18 | ShowReturnIcon: true,
19 | ReturnUrl: common.GetReturnUrl(book.BookInfo.BookID),
20 | SetDownLoadLink: false,
21 | InShelf: false,
22 | DownLoadLink: "",
23 | SetTheme: true,
24 | FlipMode: true,
25 | ShowQuickJumpBar: common.ShowQuickJumpBar(book),
26 | QuickJumpBarBooks: common.QuickJumpBarBooks(book),
27 | })
28 | @MainArea(book)
29 | }
30 | @common.Drawer(c, book, FlipDrawerSlot(book))
31 | @common.QRCode(state.ServerStatus.ServerHost)
32 | }
33 |
--------------------------------------------------------------------------------
/templ/pages/login_page/login_page.go:
--------------------------------------------------------------------------------
1 | package login_page
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/angelofallars/htmx-go"
7 | "github.com/labstack/echo/v4"
8 | "github.com/yumenaka/comigo/templ/common"
9 | )
10 |
11 | // Handler 上传文件页面
12 | func Handler(c echo.Context) error {
13 | indexHtml := common.Html(
14 | c,
15 | LoginPage(),
16 | []string{},
17 | )
18 | // 渲染页面
19 | if err := htmx.NewResponse().RenderTempl(c.Request().Context(), c.Response().Writer, indexHtml); err != nil {
20 | // 渲染失败,返回 HTTP 500 错误。
21 | return c.NoContent(http.StatusInternalServerError)
22 | }
23 | return nil
24 | }
25 |
--------------------------------------------------------------------------------
/templ/pages/login_page/login_page.templ:
--------------------------------------------------------------------------------
1 | package login_page
2 |
3 | import (
4 | "github.com/yumenaka/comigo/templ/common"
5 | )
6 |
7 | // UploadPage 上传页面
8 | templ LoginPage() {
9 | @common.Toast()
10 | @LoginMainArea()
11 | }
--------------------------------------------------------------------------------
/templ/pages/login_page/login_page_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.887
4 | package login_page
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import (
12 | "github.com/yumenaka/comigo/templ/common"
13 | )
14 |
15 | // UploadPage 上传页面
16 | func LoginPage() templ.Component {
17 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
18 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
19 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
20 | return templ_7745c5c3_CtxErr
21 | }
22 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
23 | if !templ_7745c5c3_IsBuffer {
24 | defer func() {
25 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
26 | if templ_7745c5c3_Err == nil {
27 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
28 | }
29 | }()
30 | }
31 | ctx = templ.InitializeContext(ctx)
32 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
33 | if templ_7745c5c3_Var1 == nil {
34 | templ_7745c5c3_Var1 = templ.NopComponent
35 | }
36 | ctx = templ.ClearChildren(ctx)
37 | templ_7745c5c3_Err = common.Toast().Render(ctx, templ_7745c5c3_Buffer)
38 | if templ_7745c5c3_Err != nil {
39 | return templ_7745c5c3_Err
40 | }
41 | templ_7745c5c3_Err = LoginMainArea().Render(ctx, templ_7745c5c3_Buffer)
42 | if templ_7745c5c3_Err != nil {
43 | return templ_7745c5c3_Err
44 | }
45 | return nil
46 | })
47 | }
48 |
49 | var _ = templruntime.GeneratedTemplate
50 |
--------------------------------------------------------------------------------
/templ/pages/scroll/scroll.templ:
--------------------------------------------------------------------------------
1 | package scroll
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/model"
6 | "github.com/yumenaka/comigo/templ/common"
7 | "github.com/yumenaka/comigo/templ/state"
8 | )
9 |
10 | // ScrollPage 定义 BodyHTML
11 | templ ScrollPage(c echo.Context, book *model.Book, paginationIndex int) {
12 | @InsertData(book, state.ServerStatus)
13 | @common.Toast()
14 | if book != nil {
15 | @common.Header(
16 | common.HeaderProps{
17 | Title: common.GetPageTitle(book.BookInfo.BookID),
18 | ShowReturnIcon: true,
19 | ReturnUrl: common.GetReturnUrl(book.BookInfo.BookID),
20 | SetDownLoadLink: false,
21 | InShelf: false,
22 | DownLoadLink: "",
23 | SetTheme: true,
24 | ShowQuickJumpBar: common.ShowQuickJumpBar(book),
25 | QuickJumpBarBooks: common.QuickJumpBarBooks(book),
26 | })
27 | @MainArea(c, book, paginationIndex)
28 | }
29 | @common.Footer(state.Version)
30 | @common.Drawer(c, book, DrawerSlot(c,book))
31 | @common.QRCode(state.ServerStatus.ServerHost)
32 | }
33 |
34 | templ InsertData(bookData any, serverStatus any) {
35 | if bookData != nil {
36 | @templ.JSONScript("NowBook", bookData)
37 | }
38 | if serverStatus != nil {
39 | @templ.JSONScript("ServerStatus", serverStatus)
40 | }
41 | }
42 |
43 | templ InsertRawJSONScript(data string) {
44 |
47 | }
48 |
--------------------------------------------------------------------------------
/templ/pages/settings/bool_config.templ:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | // 关键:把同名的 hidden input 包进提交
4 | // hx-include="#boolConfig_{name} [name='{name}']"
5 | func hxBoolInclude(name string) string {
6 | return "#boolConfig_" + name + " [name='" + name + "']"
7 | }
8 |
9 | // BoolConfig 布尔类型的配置
10 | templ BoolConfig(name string, value bool, description string, saveSuccessHint bool) {
11 | if saveSuccessHint {
12 |
18 | }
19 |
20 |
21 |
22 |
23 |
41 |
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/templ/pages/settings/number_config.templ:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import "strconv"
4 |
5 | templ NumberConfig(name string, value int, description string, min int, max int, saveSuccessHint bool) {
6 | if saveSuccessHint {
7 |
13 | }
14 |
37 | }
38 |
--------------------------------------------------------------------------------
/templ/pages/settings/settings.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/angelofallars/htmx-go"
7 | "github.com/labstack/echo/v4"
8 | "github.com/yumenaka/comigo/templ/common"
9 | )
10 |
11 | func getTranslations(value string) string {
12 | return "i18next.t(\"" + value + "\")"
13 | }
14 |
15 | // PageHandler 设定页面
16 | func PageHandler(c echo.Context) error {
17 | indexHtml := common.Html(
18 | c,
19 | SettingsPage(c),
20 | []string{},
21 | )
22 | // 渲染页面
23 | if err := htmx.NewResponse().RenderTempl(c.Request().Context(), c.Response().Writer, indexHtml); err != nil {
24 | // 渲染失败,返回 HTTP 500 错误。
25 | return c.NoContent(http.StatusInternalServerError)
26 | }
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/templ/pages/settings/settings_page.templ:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/templ/state"
6 | "github.com/yumenaka/comigo/templ/common"
7 | )
8 |
9 | // SettingsPage 设置页面
10 | templ SettingsPage(c echo.Context, ) {
11 | @common.Toast()
12 | @MainArea()
13 | @common.Footer(state.Version)
14 | @common.QRCode(state.ServerStatus.ServerHost)
15 |
37 | }
--------------------------------------------------------------------------------
/templ/pages/settings/settings_tab_book.templ:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import "github.com/yumenaka/comigo/templ/state"
4 | import "github.com/yumenaka/comigo/config"
5 |
6 | // htmx 是一个用于构建现代 web 应用程序的库,它使用无需刷新页面的 AJAX 技术。请求是通过 HTTP 发送的,但是 htmx 会处理响应并更新页面的部分内容,而不是整个页面。
7 | // https://htmx.org/docs/#parameters
8 | // 默认情况下,引起请求的元素将包含其值(如果有)。如果元素是一个表单,它将包含其中所有输入的值。
9 | // 与 HTML 表单一样,输入的 name 属性用作 htmx 发送的请求中的参数名称。
10 | // 此外,如果该元素导致非 GET 请求,则将包含最近的封闭表单的所有输入的值。
11 | // 此外,还可以使用 hx-vals(like: hx-vals='{"myVal": "My Value"}')在请求中包含额外的值
12 | templ tab_book() {
13 |
14 | @StringArrayConfig("LocalStores", state.ServerConfig.LocalStores, "LocalStores_Description",false)
15 | @NumberConfig("MaxScanDepth", state.ServerConfig.MaxScanDepth,"MaxScanDepth_Description",0,65535,false)
16 | @NumberConfig("MinImageNum", state.ServerConfig.MinImageNum,"MinImageNum_Description",0,65535,false)
17 | @BoolConfig("OpenBrowser",state.ServerConfig.OpenBrowser, "OpenBrowser_Description",false)
18 | @BoolConfig("EnableUpload",state.ServerConfig.EnableUpload, "EnableUpload_Description",false)
19 | @StringConfig("UploadPath", state.ServerConfig.UploadPath, "UploadPath_Description",false)
20 | @StringArrayConfig("ExcludePath", state.ServerConfig.ExcludePath, "ExcludePath_Description",false)
21 | @StringArrayConfig("SupportMediaType", state.ServerConfig.SupportMediaType, "SupportMediaType_Description",false)
22 | @StringArrayConfig("SupportFileType", state.ServerConfig.SupportFileType, "SupportFileType_Description",false)
23 | @ConfigManager(config.DefaultConfigLocation(),config.GetWorkingDirectoryConfig(), config.GetHomeDirectoryConfig(), config.GetProgramDirectoryConfig())
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/templ/pages/settings/settings_tab_labs.templ:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import "github.com/yumenaka/comigo/templ/state"
4 | import "github.com/yumenaka/comigo/config"
5 |
6 | templ tab_labs() {
7 |
8 | @BoolConfig("Debug",state.ServerConfig.Debug,"Debug_Description",false)
9 | @BoolConfig("EnableDatabase",state.ServerConfig.EnableDatabase,"EEnableDatabase_Description",false)
10 | @BoolConfig("LogToFile",state.ServerConfig.LogToFile,"LogToFile_Description",false)
11 | @BoolConfig("GenerateMetaData",state.ServerConfig.GenerateMetaData,"GenerateMetaData_Description",false)
12 | @BoolConfig("ClearCacheExit",state.ServerConfig.ClearCacheExit,"ClearCacheExit_Description",false)
13 | @StringConfig("CachePath", state.ServerConfig.CachePath,"CachePath_Description",false)
14 | @StringConfig("ZipFileTextEncoding", state.ServerConfig.ZipFileTextEncoding, "ZipFileTextEncoding_Description",false)
15 | @ConfigManager(config.DefaultConfigLocation(),config.GetWorkingDirectoryConfig(), config.GetHomeDirectoryConfig(), config.GetProgramDirectoryConfig())
16 |
17 | }
--------------------------------------------------------------------------------
/templ/pages/settings/settings_tab_network.templ:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import "github.com/yumenaka/comigo/templ/state"
4 | import "github.com/yumenaka/comigo/config"
5 |
6 | // @StringConfig("Username",state.ServerConfig.Username, "Username_Description",true)
7 | // @StringConfig("Password",state.ServerConfig.Password, "Password_Description",true)
8 |
9 | templ tab_network() {
10 |
11 | @UserInfoConfig(state.ServerConfig.Username, state.ServerConfig.Password,false)
12 | @NumberConfig("Port",state.ServerConfig.Port,"Port_Description",0,65535,false)
13 | @StringConfig("Host",state.ServerConfig.Host,"Host_Description",false)
14 | @BoolConfig("DisableLAN",state.ServerConfig.DisableLAN,"DisableLAN_Description",false)
15 | @NumberConfig("Timeout",state.ServerConfig.Timeout,"Timeout_Description",0,65535,false)
16 | @ConfigManager(config.DefaultConfigLocation(),config.GetWorkingDirectoryConfig(), config.GetHomeDirectoryConfig(), config.GetProgramDirectoryConfig())
17 |
18 | }
--------------------------------------------------------------------------------
/templ/pages/settings/string_config.templ:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | // 关键 htmx 配置解说
4 | // hx-post 请求地址
5 | // hx-trigger 值发生变化后触发
6 | // hx-target用返回的 HTML 替换哪个 DOM 元素
7 | // hx-swap 替换方式
8 | // hx-params="*" 发送表单数据,包含所有 name/value
9 | templ StringConfig(name string, value string, description string, saveSuccessHint bool) {
10 | if saveSuccessHint {
11 |
17 | }
18 |
44 | }
45 |
--------------------------------------------------------------------------------
/templ/pages/shelf/shelf.templ:
--------------------------------------------------------------------------------
1 | package shelf
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/templ/state"
6 | "github.com/yumenaka/comigo/templ/common"
7 | )
8 |
9 | // ShelfPage 书架页面
10 | templ ShelfPage(c echo.Context) {
11 | @common.Toast()
12 | @common.Header(
13 | common.HeaderProps{
14 | Title: common.GetPageTitle(c.Param("id")),
15 | ShowReturnIcon: c.Param("id") != "",
16 | ReturnUrl: common.GetReturnUrl(c.Param("id")),
17 | SetDownLoadLink: false,
18 | InShelf: true,
19 | DownLoadLink: "",
20 | SetTheme: true,
21 | })
22 | @MainArea(c)
23 | @common.Footer(state.Version)
24 | @common.Drawer(c, nil, ShelfDrawerSlot())
25 | @common.QRCode(state.ServerStatus.ServerHost)
26 | }
27 |
--------------------------------------------------------------------------------
/templ/pages/upload_page/upload.go:
--------------------------------------------------------------------------------
1 | package upload_page
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/angelofallars/htmx-go"
7 | "github.com/labstack/echo/v4"
8 | "github.com/yumenaka/comigo/templ/common"
9 | )
10 |
11 | // PageHandler 上传文件页面
12 | func PageHandler(c echo.Context) error {
13 | indexHtml := common.Html(
14 | c,
15 | UploadPage(c),
16 | []string{},
17 | )
18 | // 渲染页面
19 | if err := htmx.NewResponse().RenderTempl(c.Request().Context(), c.Response().Writer, indexHtml); err != nil {
20 | // 渲染失败,返回 HTTP 500 错误。
21 | return c.NoContent(http.StatusInternalServerError)
22 | }
23 | return nil
24 | }
25 |
--------------------------------------------------------------------------------
/templ/pages/upload_page/upload_page.templ:
--------------------------------------------------------------------------------
1 | package upload_page
2 |
3 | import (
4 | "github.com/labstack/echo/v4"
5 | "github.com/yumenaka/comigo/templ/state"
6 | "github.com/yumenaka/comigo/templ/common"
7 | )
8 |
9 | // UploadPage 上传页面
10 | templ UploadPage(c echo.Context) {
11 | @common.Toast()
12 | @common.Header(
13 | common.HeaderProps{
14 | Title: "UploadPage",
15 | ShowReturnIcon: true,
16 | ReturnUrl: "/",
17 | SetDownLoadLink: false,
18 | InShelf: false,
19 | DownLoadLink: "",
20 | SetTheme: true,
21 | })
22 | @common.UploadArea(),
23 | @common.Footer(state.Version)
24 | @common.Drawer(c, nil, nil)
25 | @common.QRCode(state.ServerStatus.ServerHost)
26 |
27 | }
--------------------------------------------------------------------------------
/templ/state/global.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "github.com/yumenaka/comigo/config"
5 | "github.com/yumenaka/comigo/model"
6 | "github.com/yumenaka/comigo/util"
7 | )
8 |
9 | // 感觉这个抽象有点多余?
10 | // type GlobalState struct {
11 | // Version string
12 | // NowBookList *model.BookInfoList
13 | // ServerStatus *util.ServerStatus
14 | // }
15 | // var Global GlobalState
16 |
17 | var (
18 | Version string
19 | ServerStatus *util.ServerStatus
20 | ServerConfig *config.Config
21 | NowBookList *model.BookInfoList
22 | )
23 |
24 | // GetNowBookNum 获取当前显示书籍数量
25 | func GetNowBookNum() int {
26 | if NowBookList == nil {
27 | return 0
28 | }
29 | return len(NowBookList.BookInfos)
30 | }
31 |
32 | // IsLogin 判断是否登录
33 | func IsLogin() bool {
34 | return config.GetUsername() != "" && config.GetPassword() != ""
35 | }
36 |
37 | // 初始化参数
38 | func init() {
39 | Version = config.GetVersion()
40 | NowBookList = nil
41 | ServerStatus = util.GetServerInfo(config.GetHost(), config.GetVersion(), config.GetPort(), config.GetEnableUpload(), 0)
42 | ServerConfig = config.GetCfg()
43 | }
44 |
--------------------------------------------------------------------------------
/tui.air.toml:
--------------------------------------------------------------------------------
1 | # 既に git 管理しているファイルをあえて無視したい:
2 | # git update-index --assume-unchanged .air.toml
3 | # Undo:
4 | # git update-index --no-assume-unchanged .air.toml
5 | root = "."
6 | testdata_dir = "test"
7 | tmp_dir = "test/temp"
8 |
9 | [build]
10 | ## 运行的时候,需要传给二进制文件的参数。
11 | # args_bin = ["./test","--login","--username=aaa","--password=aaa"]
12 | args_bin = ["./test"]
13 | #二进制文件
14 | bin = "./test/temp/comi"
15 | cmd = "go build -o ./test/temp/comi ."
16 | delay = 3000
17 | exclude_dir = ["app", "test", ".parcel-cache", "node_modules", "tmp", "bin", "testdata"]
18 | exclude_regex = ["_test\\.go", "node_modules", "_templ\\.go", "tar.gz", "zip", "tgz", "exe", "app"]
19 | exclude_unchanged = false
20 | follow_symlink = false
21 | full_bin = ""
22 | include_ext = ["go", "templ", "html", "json", "js", "ts", "css", "scss"]
23 | kill_delay = "0s"
24 | log = "test/temp/build-errors-air.log"
25 | poll = false
26 | poll_interval = 500
27 | post_cmd = []
28 | pre_cmd = ["bun run dev"]
29 | rerun = true
30 | rerun_delay = 1000
31 | send_interrupt = false
32 | stop_on_error = true
33 |
34 | [log]
35 | main_only = false
36 | silent = false
37 | time = false
38 |
39 | [color]
40 | app = ""
41 | build = "yellow"
42 | main = "magenta"
43 | runner = "green"
44 | watcher = "cyan"
45 |
46 | [misc]
47 | clean_on_exit = true
48 |
49 | [proxy]
50 | app_port = 1234
51 | enabled = true
52 | proxy_port = 7777
53 |
54 | [screen]
55 | clear_on_rebuild = false
56 | keep_scroll = true
57 |
--------------------------------------------------------------------------------
/util/file/handler_extract_file.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "io"
7 | "os"
8 | "path/filepath"
9 |
10 | "github.com/yumenaka/archives"
11 | "github.com/yumenaka/comigo/util/logger"
12 | )
13 |
14 | // extractFileHandler 解压文件的处理函数
15 | func extractFileHandler(ctx context.Context, f archives.FileInfo) error {
16 | // 从上下文中获取解压路径
17 | extractPath, ok := ctx.Value("extractPath").(string)
18 | if !ok {
19 | return errors.New("extractPath not found in context")
20 | }
21 |
22 | // 打开压缩文件中的当前文件
23 | fileReader, err := f.Open()
24 | if err != nil {
25 | logger.Infof("Failed to open file in archive: %v", err)
26 | return err
27 | }
28 | defer fileReader.Close()
29 |
30 | // 目标文件路径
31 | targetPath := filepath.Join(extractPath, f.NameInArchive)
32 |
33 | // 如果是目录,创建目录并返回
34 | if f.IsDir() {
35 | err := os.MkdirAll(targetPath, os.ModePerm)
36 | if err != nil {
37 | logger.Infof("Failed to create directory: %v", err)
38 | return err
39 | }
40 | return nil
41 | }
42 |
43 | // 确保目标文件所在的目录存在
44 | err = os.MkdirAll(filepath.Dir(targetPath), os.ModePerm)
45 | if err != nil {
46 | logger.Infof("Failed to create parent directory: %v", err)
47 | return err
48 | }
49 |
50 | // 创建目标文件
51 | destFile, err := os.Create(targetPath)
52 | if err != nil {
53 | logger.Infof("Failed to create file: %v", err)
54 | return err
55 | }
56 | defer destFile.Close()
57 |
58 | // 将文件内容从压缩包复制到目标文件
59 | _, err = io.Copy(destFile, fileReader)
60 | if err != nil {
61 | logger.Infof("Failed to copy file content: %v", err)
62 | return err
63 | }
64 |
65 | return nil
66 | }
67 |
--------------------------------------------------------------------------------
/util/file/scanNonUTF8Zip.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "os"
7 |
8 | "github.com/klauspost/compress/zip"
9 | "github.com/yumenaka/archives"
10 | "github.com/yumenaka/comigo/util/encoding"
11 | "github.com/yumenaka/comigo/util/logger"
12 | )
13 |
14 | // ScanNonUTF8Zip 扫描文件,初始化书籍用
15 | func ScanNonUTF8Zip(filePath string, textEncoding string) (reader *zip.Reader, err error) {
16 | // 打开文件,只读模式
17 | file, err := os.OpenFile(filePath, os.O_RDONLY, 0o400) // Use mode 0400 for a read-only // file and 0600 for a readable+writable file.
18 | if err != nil {
19 | logger.Infof("%s", err)
20 | }
21 | defer func(file *os.File) {
22 | err := file.Close()
23 | if err != nil {
24 | logger.Infof("file.Close() Error:%s", err)
25 | }
26 | }(file)
27 | // 是否是压缩包
28 | format, _, err := archives.Identify(context.Background(), filePath, file)
29 | if err != nil {
30 | return nil, err
31 | }
32 | // 如果是zip
33 | if ex, ok := format.(archives.Zip); ok {
34 | if textEncoding != "" {
35 | ex.TextEncoding = encoding.ByName(textEncoding)
36 | }
37 | ctx := context.Background()
38 | reader, err := ex.CheckNonUTF8Zip(ctx, file, func(ctx context.Context, f archives.FileInfo) error {
39 | // logger.Infof(f.title())
40 | return nil
41 | })
42 | if err != nil {
43 | return nil, err
44 | }
45 | return reader, err
46 | }
47 | return nil, errors.New("扫描文件错误")
48 | }
49 |
--------------------------------------------------------------------------------
/util/file/unArchiveRar.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "os"
7 |
8 | "github.com/yumenaka/archives"
9 | "github.com/yumenaka/comigo/util"
10 | "github.com/yumenaka/comigo/util/logger"
11 | )
12 |
13 | // UnArchiveRar 一次性解压 RAR 文件
14 | func UnArchiveRar(filePath string, extractPath string) error {
15 | extractPath = util.GetAbsPath(extractPath)
16 | // 如果解压路径不存在,创建路径
17 | err := os.MkdirAll(extractPath, os.ModePerm)
18 | if err != nil {
19 | logger.Infof("Failed to create extract path: %v", err)
20 | return err
21 | }
22 |
23 | // 打开文件,读模式
24 | file, err := os.Open(filePath)
25 | if err != nil {
26 | logger.Infof("Failed to open file: %v", err)
27 | return err
28 | }
29 | defer file.Close()
30 |
31 | // 确认文件格式
32 | format, _, err := archives.Identify(context.Background(), filePath, file)
33 | if err != nil {
34 | logger.Infof("Failed to identify file format: %v", err)
35 | return err
36 | }
37 |
38 | // 如果是 RAR 文件
39 | if rarFormat, ok := format.(archives.Rar); ok {
40 | ctx := context.WithValue(context.Background(), "extractPath", extractPath)
41 |
42 | err := rarFormat.Extract(ctx, file, extractFileHandler)
43 | if err != nil {
44 | logger.Infof("Failed to extract RAR file: %v", err)
45 | return err
46 | }
47 | logger.Infof("RAR 文件解压完成:%s 解压到:%s", util.GetAbsPath(filePath), extractPath)
48 | } else {
49 | logger.Infof("File is not a RAR archive: %s", filePath)
50 | return errors.New("file is not a RAR archive")
51 | }
52 |
53 | return nil
54 | }
55 |
--------------------------------------------------------------------------------
/util/file/unArchiveZip.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "os"
7 |
8 | "github.com/yumenaka/archives"
9 | "github.com/yumenaka/comigo/util"
10 | "github.com/yumenaka/comigo/util/encoding"
11 | "github.com/yumenaka/comigo/util/logger"
12 | )
13 |
14 | // UnArchiveZip 一次性解压 ZIP 文件
15 | func UnArchiveZip(filePath string, extractPath string, textEncoding string) error {
16 | extractPath = util.GetAbsPath(extractPath)
17 | // 如果解压路径不存在,创建路径
18 | err := os.MkdirAll(extractPath, os.ModePerm)
19 | if err != nil {
20 | logger.Infof("Failed to create extract path: %v", err)
21 | return err
22 | }
23 |
24 | // 打开文件,只读模式
25 | file, err := os.Open(filePath)
26 | if err != nil {
27 | logger.Infof("Failed to open file: %v", err)
28 | return err
29 | }
30 | defer file.Close()
31 |
32 | // 确认文件格式
33 | format, _, err := archives.Identify(context.Background(), filePath, file)
34 | if err != nil {
35 | logger.Infof("Failed to identify file format: %v", err)
36 | return err
37 | }
38 |
39 | // 如果是 ZIP 文件 TODO:测试文件解压
40 | if zipFormat, ok := format.(archives.Zip); ok {
41 | if textEncoding != "" {
42 | zipFormat.TextEncoding = encoding.ByName(textEncoding)
43 | }
44 | ctx := context.WithValue(context.Background(), "extractPath", extractPath)
45 |
46 | err := zipFormat.Extract(ctx, file, extractFileHandler)
47 | if err != nil {
48 | logger.Infof("Failed to extract zip file: %v", err)
49 | return err
50 | }
51 | logger.Infof("ZIP 文件解压完成:%s 解压到:%s", util.GetAbsPath(filePath), extractPath)
52 | } else {
53 | logger.Infof("File is not a ZIP archive: %s", filePath)
54 | return errors.New("file is not a ZIP archive")
55 | }
56 |
57 | return nil
58 | }
59 |
--------------------------------------------------------------------------------
/util/get_author.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | func GetAuthor(input string) string {
8 | pairs := map[rune]rune{
9 | '[': ']', // 这里的中括号是半角的
10 | '[': ']', // 这里的中括号是全角的
11 | '【': '】',
12 | '「': '」',
13 | '『': '』',
14 | '(': ')',
15 | '(': ')',
16 | '{': '}',
17 | '<': '>',
18 | '<': '>',
19 | '《': '》',
20 | '〈': '〉',
21 | '〔': '〕',
22 | '〖': '〗',
23 | }
24 |
25 | for open, closed := range pairs {
26 | if strings.HasPrefix(input, string(open)) {
27 | closeIndex := strings.IndexRune(input, closed)
28 | if closeIndex != -1 {
29 | return input[1:closeIndex]
30 | }
31 | }
32 | }
33 |
34 | pairsError := map[rune]rune{
35 | '[': ']', // 半角——全角
36 | '[': ']', // 全角——半角
37 | }
38 | for open, closed := range pairsError {
39 | if strings.HasPrefix(input, string(open)) {
40 | closeIndex := strings.IndexRune(input, closed)
41 | if closeIndex != -1 {
42 | return input[1:closeIndex]
43 | }
44 | }
45 | }
46 | return ""
47 | }
48 |
--------------------------------------------------------------------------------
/util/md5.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | )
7 |
8 | // Md5string 计算字符串的 MD5 值
9 | func Md5string(s string) string {
10 | r := md5.Sum([]byte(s))
11 | return hex.EncodeToString(r[:])
12 | }
13 |
--------------------------------------------------------------------------------
/util/scan/dir_to_book.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "net/url"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/yumenaka/comigo/model"
9 | "github.com/yumenaka/comigo/util/logger"
10 | )
11 |
12 | // 扫描目录,并返回对应书籍
13 | func scanDirGetBook(dirPath string, storePath string, depth int, option Option) (*model.Book, error) {
14 | // 获取文件夹信息
15 | dirInfo, err := os.Stat(dirPath)
16 | if err != nil {
17 | return nil, err
18 | }
19 | newBook, err := model.NewBook(dirPath, dirInfo.ModTime(), dirInfo.Size(), storePath, depth, model.TypeDir)
20 | if err != nil {
21 | return nil, err
22 | }
23 |
24 | entries, err := os.ReadDir(dirPath)
25 | if err != nil {
26 | logger.Infof("Failed to read directory: %s, error: %v", dirPath, err)
27 | return nil, err
28 | }
29 |
30 | for _, entry := range entries {
31 | if entry.IsDir() {
32 | continue
33 | }
34 |
35 | fileName := entry.Name()
36 | if !option.IsSupportMedia(fileName) {
37 | continue
38 | }
39 |
40 | fileInfo, err := entry.Info()
41 | if err != nil {
42 | logger.Infof("Failed to get file info: %s, error: %v", fileName, err)
43 | continue
44 | }
45 |
46 | absPath := filepath.Join(dirPath, fileName)
47 | tempURL := "/api/get_file?id=" + newBook.BookID + "&filename=" + url.QueryEscape(fileName)
48 | newBook.Pages.Images = append(newBook.Pages.Images, model.MediaFileInfo{
49 | Path: absPath,
50 | Size: fileInfo.Size(),
51 | ModTime: fileInfo.ModTime(),
52 | Name: fileName,
53 | Url: tempURL,
54 | })
55 | }
56 |
57 | newBook.SortPages("default")
58 | return newBook, nil
59 | }
60 |
--------------------------------------------------------------------------------
/util/scan/file_to_book.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/yumenaka/comigo/model"
7 | "github.com/yumenaka/comigo/util/logger"
8 | )
9 |
10 | // 扫描本地文件,并返回对应书籍
11 | func scanFileGetBook(filePath string, storePath string, depth int, scanOption Option) (*model.Book, error) {
12 | file, err := os.Open(filePath)
13 | if err != nil {
14 | logger.Infof("Failed to open file: %s, error: %v", filePath, err)
15 | return nil, err
16 | }
17 | defer file.Close()
18 |
19 | fileInfo, err := file.Stat()
20 | if err != nil {
21 | logger.Infof("Failed to get file info: %s, error: %v", filePath, err)
22 | return nil, err
23 | }
24 |
25 | newBook, err := model.NewBook(filePath, fileInfo.ModTime(), fileInfo.Size(), storePath, depth, model.GetBookTypeByFilename(filePath))
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | switch newBook.Type {
31 | case model.TypeZip, model.TypeCbz, model.TypeEpub:
32 | err = handleZipAndEpubFiles(filePath, newBook, scanOption)
33 | if err != nil {
34 | return nil, err
35 | }
36 | case model.TypePDF:
37 | err = handlePdfFiles(filePath, newBook)
38 | if err != nil {
39 | return nil, err
40 | }
41 | case model.TypeVideo, model.TypeAudio:
42 | handleMediaFiles(newBook)
43 | case model.TypeUnknownFile:
44 | handleMediaFiles(newBook)
45 | default:
46 | err = handleOtherArchiveFiles(filePath, newBook, scanOption)
47 | if err != nil {
48 | return nil, err
49 | }
50 | }
51 |
52 | newBook.SortPages("default")
53 | return newBook, nil
54 | }
55 |
--------------------------------------------------------------------------------
/util/scan/handle_media_file.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import "github.com/yumenaka/comigo/model"
4 |
5 | // 处理视频、音频等媒体文件
6 | func handleMediaFiles(newBook *model.Book) {
7 | newBook.PageCount = 1
8 | newBook.InitComplete = true
9 | }
10 |
--------------------------------------------------------------------------------
/util/scan/handle_pdf.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "errors"
5 | "strconv"
6 |
7 | "github.com/yumenaka/comigo/assets/locale"
8 | "github.com/yumenaka/comigo/model"
9 | "github.com/yumenaka/comigo/util/file"
10 | "github.com/yumenaka/comigo/util/logger"
11 | )
12 |
13 | // 处理 PDF 文件
14 | func handlePdfFiles(filePath string, newBook *model.Book) error {
15 | pageCount, err := file.CountPagesOfPDF(filePath)
16 | if err != nil {
17 | return err
18 | }
19 | if pageCount < 1 {
20 | return errors.New(locale.GetString("no_pages_in_pdf") + filePath)
21 | }
22 | logger.Infof(locale.GetString("scan_pdf")+" %s: %d pages", filePath, pageCount)
23 | newBook.PageCount = pageCount
24 | newBook.InitComplete = true
25 | for i := 1; i <= pageCount; i++ {
26 | tempURL := "/api/get_file?id=" + newBook.BookID + "&filename=" + strconv.Itoa(i) + ".jpg"
27 | newBook.Pages.Images = append(newBook.Pages.Images, model.MediaFileInfo{
28 | Name: strconv.Itoa(i),
29 | Url: tempURL,
30 | })
31 | }
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/util/scan/init_stores.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "github.com/yumenaka/comigo/assets/locale"
5 | "github.com/yumenaka/comigo/model"
6 | "github.com/yumenaka/comigo/util/logger"
7 | )
8 |
9 | // InitAllStore 扫描书库路径,取得书籍
10 | func InitAllStore(option Option) error {
11 | // 重置所有书籍与书组信息
12 | model.ClearAllBookData()
13 | stores := option.Cfg.GetLocalStores()
14 | // logger.Info("--------------------new stores------------------------------")
15 | // logger.Info(stores)
16 | // logger.Info("--------------------new stores------------------------------")
17 | for _, localPath := range stores {
18 | books, err := InitStore(localPath, option)
19 | if err != nil {
20 | logger.Infof(locale.GetString("scan_error")+" path:%s %s", localPath, err)
21 | continue
22 | }
23 | AddBooksToStore(books, localPath, option.Cfg.GetMinImageNum())
24 | }
25 | return nil
26 | }
27 |
28 | // AddBooksToStore 添加一组书到书库
29 | func AddBooksToStore(bookList []*model.Book, basePath string, MinImageNum int) {
30 | err := model.AddBooks(bookList, basePath, MinImageNum)
31 | if err != nil {
32 | logger.Infof(locale.GetString("AddBook_error")+"%s", basePath)
33 | }
34 | // 生成虚拟书籍组
35 | if err := model.MainStore.AnalyzeStore(); err != nil {
36 | logger.Infof("%s", err)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/util/scan/utils.go:
--------------------------------------------------------------------------------
1 | package scan
2 |
3 | import (
4 | "github.com/yumenaka/comigo/internal/database"
5 | "github.com/yumenaka/comigo/model"
6 | "github.com/yumenaka/comigo/util/logger"
7 | )
8 |
9 | // SaveResultsToDatabase 4,保存扫描结果到数据库,并清理不存在的书籍
10 | func SaveResultsToDatabase(ConfigPath string, ClearDatabaseWhenExit bool) error {
11 | err := database.InitDatabase(ConfigPath)
12 | if err != nil {
13 | return err
14 | }
15 | saveErr := database.SaveBookListToDatabase(model.GetArchiveBooks())
16 | if saveErr != nil {
17 | logger.Info(saveErr)
18 | return saveErr
19 | }
20 | return nil
21 | }
22 |
23 | func ClearDatabaseWhenExit(ConfigPath string) {
24 | AllBook := model.GetAllBookList()
25 | for _, b := range AllBook {
26 | database.ClearBookData(b)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/versioninfo.json:
--------------------------------------------------------------------------------
1 | {
2 | "FixedFileInfo": {
3 | "FileVersion": {
4 | "Major": 1,
5 | "Minor": 0,
6 | "Patch": 0,
7 | "Build": 0
8 | },
9 | "ProductVersion": {
10 | "Major": 1,
11 | "Minor": 0,
12 | "Patch": 0,
13 | "Build": 0
14 | },
15 | "FileFlagsMask": "3f",
16 | "FileFlags ": "00",
17 | "FileOS": "040004",
18 | "FileType": "01",
19 | "FileSubType": "00"
20 | },
21 | "StringFileInfo": {
22 | "Comments": "Comigo: Simple comic book reader.",
23 | "CompanyName": "",
24 | "FileDescription": "",
25 | "FileVersion": "",
26 | "InternalName": "comi.exe",
27 | "LegalCopyright": "yumenaka.net",
28 | "LegalTrademarks": "",
29 | "OriginalFilename": "comi.exe",
30 | "PrivateBuild": "",
31 | "ProductName": "",
32 | "ProductVersion": "v0.3.0",
33 | "SpecialBuild": ""
34 | },
35 | "VarFileInfo": {
36 | "Translation": {
37 | "LangID": "0409",
38 | "CharsetID": "04B0"
39 | }
40 | },
41 | "IconPath": "icon.ico",
42 | "ManifestPath": ""
43 | }
44 |
--------------------------------------------------------------------------------