├── .air.toml ├── .gitattributes ├── .gitignore ├── .postcssrc ├── LICENSE ├── Makefile ├── README.md ├── README_CN.md ├── README_JP.md ├── app ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ └── icon │ │ ├── icon-transparent.png │ │ └── icon-white.png ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-50x50@1x.png │ │ │ │ ├── Icon-App-50x50@2x.png │ │ │ │ ├── Icon-App-57x57@1x.png │ │ │ │ ├── Icon-App-57x57@2x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-72x72@1x.png │ │ │ │ ├── Icon-App-72x72@2x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── comigo_icon_512.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ ├── common │ │ └── theme.dart │ ├── components │ │ ├── bottom.dart │ │ └── header.dart │ ├── main.dart │ ├── models │ │ ├── book.dart │ │ ├── page_info.dart │ │ ├── remote_server.dart │ │ └── setting.dart │ └── pages │ │ ├── book_shelf.dart │ │ ├── flip_mode.dart │ │ └── scroll_mode.dart ├── linux │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ └── runner │ │ ├── CMakeLists.txt │ │ ├── main.cc │ │ ├── my_application.cc │ │ └── my_application.h ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ │ └── MainMenu.xib │ │ ├── Configs │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ └── RunnerTests │ │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── test │ └── widget_test.dart ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── index.html │ └── manifest.json └── windows │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake │ └── runner │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── resources │ └── app_icon.ico │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── assets ├── embed.go ├── images │ ├── ArrowBigLeft.svg │ ├── ArrowBigRight.svg │ ├── ArrowLeft.png │ ├── ArrowRight.png │ ├── Prohibited28Filled.png │ ├── SettingsOutline.png │ ├── SettingsOutline.svg │ ├── apple-touch-icon.png │ ├── audio.png │ ├── ball-triangle.svg │ ├── bars.svg │ ├── bookmark.svg │ ├── circles.svg │ ├── error.jpg │ ├── favicon-smail.png │ ├── favicon.ico │ ├── favicon.png │ ├── favicon.svg │ ├── gowebly.svg │ ├── grid.svg │ ├── home_directory.png │ ├── loading.gif │ ├── loading.jpg │ ├── manifest-desktop-screenshot.jpg │ ├── manifest-mobile-screenshot.jpg │ ├── manifest-touch-icon.svg │ ├── manifest.webmanifest │ ├── not_found.png │ ├── oval.svg │ ├── pdf.png │ ├── program_directory.png │ ├── puff.svg │ ├── rar.png │ ├── rings.svg │ ├── tail-spin.svg │ ├── three-dots.svg │ ├── unknown.png │ ├── video.png │ ├── working_directory.png │ └── zip.png ├── locale │ ├── en_US.json │ ├── ja_JP.json │ ├── localization.go │ └── zh_CN.json ├── main.js ├── plugins │ ├── alpine.js │ ├── i18n.js │ └── screenfull.js ├── script │ ├── flip.css │ ├── flip.js │ ├── flip_sketch.js │ ├── main.js │ ├── scroll.js │ ├── shelf.js │ └── styles.css ├── stores │ ├── cookie_store.js │ ├── flip_store.js │ ├── global_store.js │ ├── scroll_store.js │ ├── shelf_store.js │ └── theme_store.js ├── styles.css └── utils │ └── imageParameters.js ├── bun.lock ├── cmd ├── broadcast.go ├── comi │ └── main.go ├── config_reload_handler.go ├── experiments │ └── switch_port.go ├── image_viewer │ ├── Readme.md │ ├── ent │ │ ├── client.go │ │ ├── directory.go │ │ ├── directory │ │ │ ├── directory.go │ │ │ └── where.go │ │ ├── directory_create.go │ │ ├── directory_delete.go │ │ ├── directory_query.go │ │ ├── directory_update.go │ │ ├── ent.go │ │ ├── enttest │ │ │ └── enttest.go │ │ ├── hook │ │ │ └── hook.go │ │ ├── image.go │ │ ├── image │ │ │ ├── image.go │ │ │ └── where.go │ │ ├── image_create.go │ │ ├── image_delete.go │ │ ├── image_query.go │ │ ├── image_update.go │ │ ├── migrate │ │ │ ├── migrate.go │ │ │ └── schema.go │ │ ├── mutation.go │ │ ├── predicate │ │ │ └── predicate.go │ │ ├── runtime.go │ │ ├── runtime │ │ │ └── runtime.go │ │ ├── schema │ │ │ ├── directory.go │ │ │ └── image.go │ │ └── tx.go │ ├── handlers.go │ ├── main.go │ ├── models.go │ ├── scanner.go │ ├── static_files │ │ └── index.html │ ├── storage.go │ └── utils.go ├── init_flags.go ├── mobile │ ├── Readme.md │ └── main.go ├── root.go ├── set_daemon.go ├── set_shutdown_hander.go ├── set_store_path.go ├── set_stores.go ├── show_qrcode.go ├── tui │ ├── tea.go │ └── tool.go └── wasm │ ├── index.html │ └── main.go ├── config ├── common.go ├── config.go ├── get_set.go ├── init.go ├── plugin.go ├── status.go ├── stores │ └── store.go └── version.go ├── get_comigo.sh ├── go.mod ├── go.sum ├── goversioninfo.exe.manifest ├── icon.ico ├── internal ├── database │ ├── book_crud.go │ ├── fake_js_wasm.go │ ├── fake_windows_386.go │ └── init.go └── ent │ ├── Readme.md │ ├── book.go │ ├── book │ ├── book.go │ └── where.go │ ├── book_create.go │ ├── book_delete.go │ ├── book_query.go │ ├── book_update.go │ ├── client.go │ ├── ent.go │ ├── enttest │ └── enttest.go │ ├── generate.go │ ├── hook │ └── hook.go │ ├── migrate │ ├── migrate.go │ └── schema.go │ ├── mutation.go │ ├── predicate │ └── predicate.go │ ├── runtime.go │ ├── runtime │ └── runtime.go │ ├── schema │ ├── book.go │ ├── singlepageinfo.go │ └── user.go │ ├── singlepageinfo.go │ ├── singlepageinfo │ ├── singlepageinfo.go │ └── where.go │ ├── singlepageinfo_create.go │ ├── singlepageinfo_delete.go │ ├── singlepageinfo_query.go │ ├── singlepageinfo_update.go │ ├── tx.go │ ├── user.go │ ├── user │ ├── user.go │ └── where.go │ ├── user_create.go │ ├── user_delete.go │ ├── user_query.go │ └── user_update.go ├── main.go ├── model ├── book.go ├── book_group.go ├── book_info.go ├── book_info_list.go ├── book_util.go ├── dir_node.go ├── folder.go ├── image_info.go ├── support_file_type.go ├── tool.go └── user.go ├── package.json ├── routers ├── config_api │ ├── delete_config.go │ ├── get_config.go │ ├── get_config_status.go │ ├── save_config.go │ └── update_config.go ├── get_data_api │ ├── Roboto-Medium.ttf │ ├── get_book.go │ ├── get_book_info.go │ ├── get_file.go │ ├── get_generated_image.go │ ├── get_qrcode.go │ ├── get_raw_file.go │ ├── get_reg_file.go │ └── get_status.go ├── login │ └── jwt.go ├── reverse_proxy │ └── reverse_proxy.go ├── router.go ├── set_cache.go ├── set_log.go ├── set_port.go ├── start_echo.go ├── upload_api │ └── upload_file.go ├── urls.go └── websocket │ ├── handMessage.go │ └── handler.go ├── tailwind.config.js ├── templ ├── common │ ├── common.go │ ├── drawer.templ │ ├── drawer_templ.go │ ├── footer.templ │ ├── footer_templ.go │ ├── header.templ │ ├── header_templ.go │ ├── html.templ │ ├── html_templ.go │ ├── message.templ │ ├── message_templ.go │ ├── qrcode.templ │ ├── qrcode_templ.go │ ├── svg │ │ ├── arror_down.templ │ │ ├── arror_down_templ.go │ │ ├── book.templ │ │ ├── book_templ.go │ │ ├── close.templ │ │ ├── close_templ.go │ │ ├── delete.templ │ │ ├── delete_templ.go │ │ ├── fullscreen.templ │ │ ├── fullscreen_templ.go │ │ ├── labs.templ │ │ ├── labs_templ.go │ │ ├── network.templ │ │ ├── network_templ.go │ │ ├── qrcode.templ │ │ ├── qrcode_templ.go │ │ ├── return.templ │ │ ├── return_templ.go │ │ ├── server_disk.templ │ │ ├── server_disk_templ.go │ │ ├── setting.templ │ │ ├── setting_templ.go │ │ ├── upload.templ │ │ └── upload_templ.go │ ├── toast.templ │ ├── toast_templ.go │ ├── upload_area.templ │ └── upload_area_templ.go ├── pages │ ├── error_page │ │ ├── not_found.go │ │ ├── not_found.templ │ │ └── not_found_templ.go │ ├── flip │ │ ├── flip.go │ │ ├── flip.templ │ │ ├── flip_drawer_slot.templ │ │ ├── flip_drawer_slot_templ.go │ │ ├── flip_main_area.templ │ │ ├── flip_main_area_templ.go │ │ ├── flip_steps_range.templ │ │ ├── flip_steps_range_templ.go │ │ └── flip_templ.go │ ├── login_page │ │ ├── login_main_area.templ │ │ ├── login_main_area_templ.go │ │ ├── login_page.go │ │ ├── login_page.templ │ │ └── login_page_templ.go │ ├── scroll │ │ ├── scroll.go │ │ ├── scroll.templ │ │ ├── scroll_drawer_slot.templ │ │ ├── scroll_drawer_slot_templ.go │ │ ├── scroll_main_area.templ │ │ ├── scroll_main_area_templ.go │ │ ├── scroll_pagination.templ │ │ ├── scroll_pagination_templ.go │ │ └── scroll_templ.go │ ├── settings │ │ ├── api.go │ │ ├── before_update.go │ │ ├── bool_config.templ │ │ ├── bool_config_templ.go │ │ ├── config_manager.templ │ │ ├── config_manager_templ.go │ │ ├── number_config.templ │ │ ├── number_config_templ.go │ │ ├── settings.go │ │ ├── settings_main_area.templ │ │ ├── settings_main_area_templ.go │ │ ├── settings_page.templ │ │ ├── settings_page_templ.go │ │ ├── settings_tab_book.templ │ │ ├── settings_tab_book_templ.go │ │ ├── settings_tab_labs.templ │ │ ├── settings_tab_labs_templ.go │ │ ├── settings_tab_network.templ │ │ ├── settings_tab_network_templ.go │ │ ├── string_config.templ │ │ ├── string_config_templ.go │ │ ├── strings_array_config.templ │ │ ├── strings_array_config_templ.go │ │ ├── user_info_config.templ │ │ └── user_info_config_templ.go │ ├── shelf │ │ ├── api.go │ │ ├── shelf.go │ │ ├── shelf.templ │ │ ├── shelf_book_card.templ │ │ ├── shelf_book_card_templ.go │ │ ├── shelf_drawer_slot.templ │ │ ├── shelf_drawer_slot_templ.go │ │ ├── shelf_main_area.templ │ │ ├── shelf_main_area_templ.go │ │ └── shelf_templ.go │ └── upload_page │ │ ├── upload.go │ │ ├── upload_page.templ │ │ └── upload_page_templ.go └── state │ └── global.go ├── todo.md ├── tui.air.toml ├── util ├── check.go ├── encoding │ └── encoding.go ├── file.go ├── file │ ├── cache.go │ ├── epub.go │ ├── get_picture_data.go │ ├── get_single_file.go │ ├── handler_extract_file.go │ ├── pdf.go │ ├── scanNonUTF8Zip.go │ ├── unArchiveRar.go │ └── unArchiveZip.go ├── get_author.go ├── image.go ├── logger │ └── logger.go ├── md5.go ├── natsort.go ├── natsort_test.go ├── plugin.go ├── qrcode.go ├── scan │ ├── define.go │ ├── dir_to_book.go │ ├── file_to_book.go │ ├── handle_directory.go │ ├── handle_media_file.go │ ├── handle_pdf.go │ ├── handle_zip.go │ ├── init_store.go │ ├── init_stores.go │ ├── scan_sftp.go │ ├── scan_smb.go │ ├── scan_webdav.go │ └── utils.go ├── self_update.go ├── string.go └── system.go └── versioninfo.json /.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 run github.com/a-h/templ/cmd/templ@latest generate && 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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://github.com/github-linguist/linguist/blob/master/docs/overrides.md 2 | 3 | #排除文档文件 4 | resource/CHANGELOG.md -linguist-documentation 5 | 6 | # 排除目录 7 | assets/images/**/* linguist-vendored 8 | 9 | #排除手机项目 10 | app/**/* linguist-vendored 11 | 12 | #排除测试文件 13 | test/* linguist-documentation 14 | 15 | #排除自动生成文件 16 | internal/ent/**/* linguist-vendored 17 | assets/script/main.js linguist-vendored 18 | assets/script/styles.css linguist-vendored 19 | assets/script/main.js.map linguist-vendored 20 | assets/script/styles.css.map linguist-vendored 21 | assets/wailsjs linguist-vendored 22 | 23 | *.templ linguist-language=Go -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@tailwindcss/postcss": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 yumenaka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | *.apk 47 | .env -------------------------------------------------------------------------------- /app/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 17 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 18 | - platform: android 19 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 20 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 21 | - platform: ios 22 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 23 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 24 | - platform: linux 25 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 26 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 27 | - platform: macos 28 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 29 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 30 | - platform: web 31 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 32 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 33 | - platform: windows 34 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 35 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | ## 开发笔记 2 | 3 | flutter_launcher_icons自动生成图标 ,可以在项目文件夹下运行此命令: 4 | ```bash 5 | dart run flutter_launcher_icons && rm -r android/app/src/main/res/mipmap-anydpi-v26 6 | ``` 7 | 8 | 重命名项目(https://pub.dev/packages/rename): 9 | ```bash 10 | flutter pub global activate rename 11 | flutter pub global run rename setAppName --targets ios,android,macos,windows,linux,web --value "Comigo" 12 | ``` 13 | -------------------------------------------------------------------------------- /app/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /app/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /app/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "net.yumenaka.comigo.app" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "net.yumenaka.comigo.app" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 6 | -------------------------------------------------------------------------------- /app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.2.2" apply false 22 | id "org.jetbrains.kotlin.android" version "2.1.0" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /app/assets/icon/icon-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/assets/icon/icon-transparent.png -------------------------------------------------------------------------------- /app/assets/icon/icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/assets/icon/icon-white.png -------------------------------------------------------------------------------- /app/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /app/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /app/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /app/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/comigo_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/comigo_icon_512.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /app/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Comigo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Comigo 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /app/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/lib/common/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | final appTheme = ThemeData( 4 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueAccent), 5 | useMaterial3: true, 6 | textTheme: const TextTheme( 7 | displayLarge: TextStyle( 8 | fontFamily: 'Corben', 9 | fontWeight: FontWeight.w700, 10 | fontSize: 24, 11 | color: Colors.black, 12 | ), 13 | ), 14 | ); -------------------------------------------------------------------------------- /app/lib/components/bottom.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/lib/components/bottom.dart -------------------------------------------------------------------------------- /app/lib/components/header.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/lib/components/header.dart -------------------------------------------------------------------------------- /app/lib/models/page_info.dart: -------------------------------------------------------------------------------- 1 | class PageInfo { 2 | String url; 3 | String filename; 4 | 5 | PageInfo({required this.url, required this.filename}); 6 | 7 | factory PageInfo.fromJson(Map json) { 8 | return PageInfo( 9 | url: json['url'], 10 | filename: json['filename'], 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/lib/models/remote_server.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class RemoteServer extends ChangeNotifier { 5 | late String remoteHost; 6 | 7 | RemoteServer({required String defaultHost}) { 8 | remoteHost= defaultHost; 9 | loadHost(defaultHost: defaultHost); 10 | } 11 | 12 | Future loadHost({required String defaultHost}) async { 13 | final prefs = await SharedPreferences.getInstance(); 14 | var h = prefs.getString('comigo_host'); 15 | if (h != null) { 16 | remoteHost = h; 17 | //模型发生改变并且需要更新 UI 的时候调用该方法 18 | notifyListeners(); 19 | } 20 | } 21 | 22 | Future saveHost(String host) async { 23 | final prefs = await SharedPreferences.getInstance(); 24 | await prefs.setString('comigo_host', host); 25 | remoteHost = host; 26 | //模型发生改变并且需要更新 UI 的时候调用该方法 27 | notifyListeners(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/lib/models/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | enum ReadMode { scrollMode, flipMode } 5 | 6 | class Setting extends ChangeNotifier { 7 | // 服务器地址,当前为远程地址 8 | String serverHost = ""; 9 | // 阅读模式,默认卷轴模式 10 | ReadMode readMode = ReadMode.scrollMode; 11 | Setting() { 12 | init(); 13 | } 14 | 15 | // 初始化设置,利用shared_preferences插件,从本地读取设置 16 | Future init() async { 17 | final prefs = await SharedPreferences.getInstance(); 18 | serverHost = prefs.getString('comigo_host') ?? "http://127.0.0.1:1234"; 19 | final mode = prefs.getString('read_mode'); 20 | if (mode == null) { 21 | readMode = ReadMode.scrollMode; 22 | } else { 23 | readMode = ReadMode.values.firstWhere((e) => e.toString() == mode); 24 | } 25 | //模型发生改变并且需要更新 UI 的时候调用该方法 26 | notifyListeners(); 27 | } 28 | 29 | // 设置服务器地址 30 | Future setHost(String host) async { 31 | final prefs = await SharedPreferences.getInstance(); 32 | await prefs.setString('comigo_host', host); 33 | serverHost = host; 34 | //模型发生改变并且需要更新 UI 的时候调用该方法 35 | notifyListeners(); 36 | } 37 | // 设置阅读模式 38 | Future setReadMode(ReadMode mode) async { 39 | final prefs = await SharedPreferences.getInstance(); 40 | await prefs.setString('read_mode', mode.toString()); 41 | readMode = mode; 42 | //模型发生改变并且需要更新 UI 的时候调用该方法 43 | notifyListeners(); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/lib/pages/flip_mode.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/lib/pages/flip_mode.dart -------------------------------------------------------------------------------- /app/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /app/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void fl_register_plugins(FlPluginRegistry* registry) { 12 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 13 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 14 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 15 | } 16 | -------------------------------------------------------------------------------- /app/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /app/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_linux 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /app/linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /app/linux/runner/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /app/linux/runner/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /app/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /app/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /app/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /app/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import shared_preferences_foundation 9 | import url_launcher_macos 10 | 11 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 12 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 13 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 14 | } 15 | -------------------------------------------------------------------------------- /app/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | }, 6 | "images": [ 7 | { 8 | "size": "16x16", 9 | "idiom": "mac", 10 | "filename": "app_icon_16.png", 11 | "scale": "1x" 12 | }, 13 | { 14 | "size": "16x16", 15 | "idiom": "mac", 16 | "filename": "app_icon_32.png", 17 | "scale": "2x" 18 | }, 19 | { 20 | "size": "32x32", 21 | "idiom": "mac", 22 | "filename": "app_icon_32.png", 23 | "scale": "1x" 24 | }, 25 | { 26 | "size": "32x32", 27 | "idiom": "mac", 28 | "filename": "app_icon_64.png", 29 | "scale": "2x" 30 | }, 31 | { 32 | "size": "128x128", 33 | "idiom": "mac", 34 | "filename": "app_icon_128.png", 35 | "scale": "1x" 36 | }, 37 | { 38 | "size": "128x128", 39 | "idiom": "mac", 40 | "filename": "app_icon_256.png", 41 | "scale": "2x" 42 | }, 43 | { 44 | "size": "256x256", 45 | "idiom": "mac", 46 | "filename": "app_icon_256.png", 47 | "scale": "1x" 48 | }, 49 | { 50 | "size": "256x256", 51 | "idiom": "mac", 52 | "filename": "app_icon_512.png", 53 | "scale": "2x" 54 | }, 55 | { 56 | "size": "512x512", 57 | "idiom": "mac", 58 | "filename": "app_icon_512.png", 59 | "scale": "1x" 60 | }, 61 | { 62 | "size": "512x512", 63 | "idiom": "mac", 64 | "filename": "app_icon_1024.png", 65 | "scale": "2x" 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /app/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = Comigo 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = net.yumenaka.comigo.app 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 net.yumenaka.comigo. All rights reserved. 15 | -------------------------------------------------------------------------------- /app/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /app/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /app/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /app/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:comigo/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const ComigoApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /app/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/web/favicon.png -------------------------------------------------------------------------------- /app/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/web/icons/Icon-192.png -------------------------------------------------------------------------------- /app/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/web/icons/Icon-512.png -------------------------------------------------------------------------------- /app/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /app/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /app/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Comigo 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "short_name": "app", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "comigo mobile app", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /app/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /app/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void RegisterPlugins(flutter::PluginRegistry* registry) { 12 | UrlLauncherWindowsRegisterWithRegistrar( 13 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 14 | } 15 | -------------------------------------------------------------------------------- /app/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /app/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | url_launcher_windows 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /app/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /app/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"Comigo", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /app/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /app/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/app/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /app/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector 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 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /assets/images/rar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumenaka/comigo/3663dd48e3d7494270fa051a31f753b858a00eb1/assets/images/rar.png -------------------------------------------------------------------------------- /assets/images/rings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | 18 | 19 | 20 | 25 | 29 | 33 | 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/images/tail-spin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /assets/images/three-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 28 | 32 | 33 | 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 | 5 | 6 | 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 | 6 | 7 | 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 | 5 | 6 | 10 | 11 | 12 | } -------------------------------------------------------------------------------- /templ/common/svg/fullscreen.templ: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | templ FullScreen() { 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 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 | 6 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /templ/common/svg/qrcode.templ: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | templ QRCode() { 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 20 | 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 | 12 | 17 | 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 | 5 | 13 | 21 | 29 | 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 |
18 | 19 | 34 |
35 |
36 |
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 |
24 | 25 | 38 |
42 |
43 |
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 | --------------------------------------------------------------------------------