├── packages
├── epubjs
│ ├── .nojekyll
│ ├── .npmignore
│ ├── test
│ │ ├── fixtures
│ │ │ ├── alice
│ │ │ │ ├── mimetype
│ │ │ │ ├── OPS
│ │ │ │ │ ├── images
│ │ │ │ │ │ ├── title.jpg
│ │ │ │ │ │ ├── cover_th.jpg
│ │ │ │ │ │ ├── i001_th.jpg
│ │ │ │ │ │ ├── i002_th.jpg
│ │ │ │ │ │ ├── i003_th.jpg
│ │ │ │ │ │ ├── i004_th.jpg
│ │ │ │ │ │ ├── i005_th.jpg
│ │ │ │ │ │ ├── i006_th.jpg
│ │ │ │ │ │ ├── i007_th.jpg
│ │ │ │ │ │ ├── i008_th.jpg
│ │ │ │ │ │ ├── i009_th.jpg
│ │ │ │ │ │ ├── i010_th.jpg
│ │ │ │ │ │ ├── i011_th.jpg
│ │ │ │ │ │ ├── i012_th.jpg
│ │ │ │ │ │ ├── i013_th.jpg
│ │ │ │ │ │ ├── i014_th.jpg
│ │ │ │ │ │ ├── i015_th.jpg
│ │ │ │ │ │ ├── i016_th.jpg
│ │ │ │ │ │ ├── i017_th.jpg
│ │ │ │ │ │ ├── i018_th.jpg
│ │ │ │ │ │ ├── i019_th.jpg
│ │ │ │ │ │ ├── i020_th.jpg
│ │ │ │ │ │ ├── i022_th.jpg
│ │ │ │ │ │ ├── ii021_th.jpg
│ │ │ │ │ │ ├── plate01_th.jpg
│ │ │ │ │ │ ├── plate02_th.jpg
│ │ │ │ │ │ ├── plate03_th.jpg
│ │ │ │ │ │ └── plate04_th.jpg
│ │ │ │ │ ├── cover.xhtml
│ │ │ │ │ ├── titlepage.xhtml
│ │ │ │ │ └── toc.xhtml
│ │ │ │ └── META-INF
│ │ │ │ │ └── container.xml
│ │ │ ├── alice.epub
│ │ │ └── alice_without_cover.epub
│ │ ├── index.html
│ │ ├── old
│ │ │ └── rendering.js
│ │ └── locations.js
│ ├── tsconfig.json
│ ├── types
│ │ ├── tslint.json
│ │ ├── utils
│ │ │ ├── scrolltype.d.ts
│ │ │ ├── request.d.ts
│ │ │ ├── constants.d.ts
│ │ │ ├── url.d.ts
│ │ │ ├── path.d.ts
│ │ │ ├── hook.d.ts
│ │ │ ├── replacements.d.ts
│ │ │ └── queue.d.ts
│ │ ├── container.d.ts
│ │ ├── epub.d.ts
│ │ ├── epubjs-tests.ts
│ │ ├── tsconfig.json
│ │ ├── index.d.ts
│ │ ├── spine.d.ts
│ │ ├── pagelist.d.ts
│ │ ├── archive.d.ts
│ │ ├── themes.d.ts
│ │ ├── resources.d.ts
│ │ ├── mapping.d.ts
│ │ ├── locations.d.ts
│ │ ├── store.d.ts
│ │ ├── layout.d.ts
│ │ ├── navigation.d.ts
│ │ └── section.d.ts
│ ├── .watchmanconfig
│ ├── documentation.yml
│ ├── examples
│ │ ├── ajax-loader.gif
│ │ ├── themes.css
│ │ ├── contents.html
│ │ └── toc.html
│ ├── .gitignore
│ ├── .travis.yml
│ ├── src
│ │ ├── index.js
│ │ ├── epub.js
│ │ └── container.js
│ ├── eslint.config.js
│ ├── .babelrc.json
│ ├── .jshintrc
│ ├── bower.json
│ └── license
├── internal
│ ├── src
│ │ ├── index.ts
│ │ └── lib.ts
│ ├── tsconfig.json
│ └── package.json
└── tailwind
│ ├── package.json
│ └── src
│ ├── state.js
│ ├── elevation.js
│ └── index.js
├── apps
├── api
│ ├── src
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ ├── seed.py
│ │ │ ├── app_config.py
│ │ │ └── db.py
│ │ ├── domain
│ │ │ ├── __init__.py
│ │ │ ├── book
│ │ │ │ ├── __init__.py
│ │ │ │ ├── value_objects
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── book_title.py
│ │ │ │ │ ├── book_id.py
│ │ │ │ │ ├── tennant_id.py
│ │ │ │ │ └── book_metadata.py
│ │ │ │ ├── entities
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── repositories
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── book_repository.py
│ │ │ │ └── exceptions
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── book_exceptions.py
│ │ │ ├── chat
│ │ │ │ ├── __init__.py
│ │ │ │ ├── entities
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── exceptions
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── chat_exceptions.py
│ │ │ │ ├── repositories
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── chat_repository.py
│ │ │ │ └── value_objects
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── chat_title.py
│ │ │ │ │ ├── book_id.py
│ │ │ │ │ ├── chat_id.py
│ │ │ │ │ └── user_id.py
│ │ │ ├── annotation
│ │ │ │ ├── __init__.py
│ │ │ │ ├── entities
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── repositories
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── annotation_repository.py
│ │ │ │ ├── value_objects
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── annotation_cfi.py
│ │ │ │ │ ├── annotation_notes.py
│ │ │ │ │ ├── annotation_text.py
│ │ │ │ │ ├── annotation_id.py
│ │ │ │ │ ├── annotation_type.py
│ │ │ │ │ └── annotation_color.py
│ │ │ │ └── exceptions.py
│ │ │ ├── podcast
│ │ │ │ ├── __init__.py
│ │ │ │ ├── entities
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── repositories
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── value_objects
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── language.py
│ │ │ │ │ ├── podcast_id.py
│ │ │ │ │ └── speaker_role.py
│ │ │ │ └── exceptions
│ │ │ │ │ └── __init__.py
│ │ │ └── message
│ │ │ │ ├── entities
│ │ │ │ └── __init__.py
│ │ │ │ ├── repositories
│ │ │ │ ├── __init__.py
│ │ │ │ └── message_repository.py
│ │ │ │ ├── value_objects
│ │ │ │ ├── __init__.py
│ │ │ │ ├── message_content.py
│ │ │ │ ├── message_id.py
│ │ │ │ └── sender_type.py
│ │ │ │ ├── exceptions
│ │ │ │ ├── __init__.py
│ │ │ │ └── message_exceptions.py
│ │ │ │ └── __init__.py
│ │ ├── infrastructure
│ │ │ ├── __init__.py
│ │ │ ├── external
│ │ │ │ ├── __init__.py
│ │ │ │ ├── cloud_tts
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── gemini
│ │ │ │ │ ├── prompts
│ │ │ │ │ │ └── __init__.py
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── epub
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── epub_reader.py
│ │ │ │ └── audio
│ │ │ │ │ └── __init__.py
│ │ │ ├── di
│ │ │ │ └── __init__.py
│ │ │ ├── postgres
│ │ │ │ ├── chat
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── user
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── podcast
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── annotation
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── book
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── message
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── __init__.py
│ │ │ │ └── db_util.py
│ │ │ ├── memory
│ │ │ │ ├── __init__.py
│ │ │ │ └── retry_decorator.py
│ │ │ └── vector.py
│ │ ├── usecase
│ │ │ ├── annotation
│ │ │ │ └── __init__.py
│ │ │ ├── podcast
│ │ │ │ ├── __init__.py
│ │ │ │ ├── find_podcasts_by_book_id_usecase.py
│ │ │ │ ├── podcast_config.py
│ │ │ │ └── find_podcast_by_id_usecase.py
│ │ │ ├── chat
│ │ │ │ ├── __init__.py
│ │ │ │ ├── find_chats_by_user_id_usecase.py
│ │ │ │ ├── delete_chat_usecase.py
│ │ │ │ ├── find_chat_by_id_usecase.py
│ │ │ │ ├── find_chats_by_user_id_and_book_id_usecase.py
│ │ │ │ ├── create_chat_usecase.py
│ │ │ │ └── update_chat_title_usecase.py
│ │ │ ├── message
│ │ │ │ ├── __init__.py
│ │ │ │ ├── find_message_by_id_usecase.py
│ │ │ │ ├── find_messages_usecase.py
│ │ │ │ └── highlight_searcher.py
│ │ │ └── book
│ │ │ │ ├── create_book_vector_index_usecase.py
│ │ │ │ ├── find_books_usecase.py
│ │ │ │ ├── find_book_by_id_usecase.py
│ │ │ │ └── __init__.py
│ │ ├── presentation
│ │ │ └── api
│ │ │ │ ├── handlers
│ │ │ │ ├── __init__.py
│ │ │ │ ├── annotation_api_route_handler.py
│ │ │ │ └── rag_api_route_handler.py
│ │ │ │ ├── schemas
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base_schema.py
│ │ │ │ ├── annotation_schema.py
│ │ │ │ └── chat_schema.py
│ │ │ │ ├── converters
│ │ │ │ └── __init__.py
│ │ │ │ ├── error_messages
│ │ │ │ ├── __init__.py
│ │ │ │ ├── chat_error_message.py
│ │ │ │ ├── message_error_message.py
│ │ │ │ └── book_error_message.py
│ │ │ │ ├── __init__.py
│ │ │ │ └── routes.py
│ │ ├── __init__.py
│ │ └── main.py
│ ├── .python-version
│ ├── gcs_fixtures
│ │ └── bookwith-bucket
│ │ │ └── .gitkeep
│ ├── package.json
│ ├── index_tenant_mapping.json
│ └── docker-compose.yml
└── reader
│ ├── .gitignore
│ ├── src
│ ├── components
│ │ ├── pages
│ │ │ └── index.ts
│ │ ├── Reader.tsx
│ │ ├── Library.tsx
│ │ ├── base
│ │ │ ├── GridView.tsx
│ │ │ ├── index.ts
│ │ │ └── ActionBar.tsx
│ │ ├── TextSelectionMenu.tsx
│ │ ├── library
│ │ │ └── index.ts
│ │ ├── textSelection
│ │ │ └── index.ts
│ │ ├── chat
│ │ │ ├── types.ts
│ │ │ └── EmptyState.tsx
│ │ ├── reader
│ │ │ ├── PaneContainer.tsx
│ │ │ ├── index.ts
│ │ │ ├── Bar.tsx
│ │ │ ├── ReaderGridView.tsx
│ │ │ ├── eventHandlers.ts
│ │ │ ├── ReaderPaneHeader.tsx
│ │ │ └── ReaderPaneFooter.tsx
│ │ ├── index.ts
│ │ ├── viewlets
│ │ │ └── PodcastView.tsx
│ │ ├── podcast
│ │ │ ├── index.ts
│ │ │ └── PodcastPane.tsx
│ │ ├── Page.tsx
│ │ ├── FormattedText.tsx
│ │ ├── ui
│ │ │ ├── textarea.tsx
│ │ │ ├── input.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── spinner.tsx
│ │ │ ├── badge.tsx
│ │ │ └── tooltip.tsx
│ │ ├── ErrorBoundary.tsx
│ │ └── Button.tsx
│ ├── models
│ │ ├── index.ts
│ │ └── tree.ts
│ ├── pages
│ │ ├── _.tsx
│ │ ├── success.tsx
│ │ └── _app.tsx
│ ├── types
│ │ ├── window.d.ts
│ │ ├── podcast.ts
│ │ └── loading.ts
│ ├── hooks
│ │ ├── theme
│ │ │ ├── index.ts
│ │ │ ├── useTheme.ts
│ │ │ ├── useSourceColor.ts
│ │ │ └── useColorScheme.ts
│ │ ├── loading
│ │ │ ├── index.ts
│ │ │ ├── useLoadingState.ts
│ │ │ └── useProgressManager.ts
│ │ ├── useForceRender.ts
│ │ ├── useEnv.ts
│ │ ├── useList.ts
│ │ ├── useAfterMount.ts
│ │ ├── useAutoResize.ts
│ │ ├── usePress.ts
│ │ ├── useAsync.ts
│ │ ├── useBoolean.ts
│ │ ├── useHover.ts
│ │ ├── useAction.ts
│ │ ├── useSWR
│ │ │ ├── fetcher.ts
│ │ │ ├── useLibrary.ts
│ │ │ ├── usePodcast.ts
│ │ │ └── useChat.ts
│ │ ├── useMobile.ts
│ │ ├── useMediaQuery.ts
│ │ ├── index.ts
│ │ ├── useDisablePinchZooming.ts
│ │ ├── useTypography.ts
│ │ ├── useIntermediateKeyword.ts
│ │ ├── useIntermediateChatKeyword.ts
│ │ ├── useTranslation.ts
│ │ ├── useLoading.ts
│ │ └── useLongPress.ts
│ ├── lib
│ │ └── utils.ts
│ ├── utils
│ │ ├── mime.ts
│ │ ├── platform.ts
│ │ ├── epub.ts
│ │ ├── state.ts
│ │ ├── color.ts
│ │ ├── fileUtils.ts
│ │ ├── utils.ts
│ │ └── annotation.ts
│ ├── store
│ │ ├── loading
│ │ │ ├── atoms.ts
│ │ │ ├── index.ts
│ │ │ └── selectors.ts
│ │ └── loading.ts
│ └── constants
│ │ ├── audio.ts
│ │ └── podcast.ts
│ ├── sentry.properties
│ ├── public
│ ├── icons
│ │ ├── 192.png
│ │ ├── 512.png
│ │ ├── maskable-192.png
│ │ └── maskable-512.png
│ └── manifest.json
│ ├── postcss.config.js
│ ├── .prettierignore
│ ├── locales
│ └── index.ts
│ ├── next-env.d.ts
│ ├── tsconfig.json
│ ├── docker-compose.yml
│ ├── .env.example
│ ├── web.d.ts
│ ├── netlify.toml
│ ├── components.json
│ ├── eslint.config.js
│ ├── sentry.client.config.js
│ └── sentry.server.config.js
├── .gitattributes
├── .dockerignore
├── pnpm-workspace.yaml
├── .husky
└── pre-commit
├── .npmrc
├── supabase
├── .gitignore
└── migrations
│ └── 20250713003643_add_language_to_podcast.sql
├── .prettierignore
├── prettier.config.cjs
├── .mcp.json
├── tsconfig.ts.json
├── poetry.lock
├── tsconfig.react.json
├── turbo.json
├── pyproject.toml
├── .pre-commit-config.yaml
├── .github
└── ISSUE_TEMPLATE
│ ├── feature-request.yml
│ └── bug_report.yml
├── tsconfig.next.json
├── tsconfig.json
├── .claude
└── settings.local.json
├── .gitignore
├── .vscode
└── settings.json
└── package.json
/packages/epubjs/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/.python-version:
--------------------------------------------------------------------------------
1 | 3.13.2
2 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/annotation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/podcast/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/gcs_fixtures/bookwith-bucket/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/entities/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/entities/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/external/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/entities/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/value_objects/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/repositories/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/value_objects/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/repositories/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/value_objects/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/handlers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/schemas/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/reader/.gitignore:
--------------------------------------------------------------------------------
1 | # Sentry
2 | .sentryclirc
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | packages/epubjs/** linguist-vendored
2 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/repositories/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/external/cloud_tts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/converters/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/error_messages/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/epubjs/.npmignore:
--------------------------------------------------------------------------------
1 | books
2 | test
3 | .babelrc
4 |
--------------------------------------------------------------------------------
/packages/internal/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib'
2 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/external/gemini/prompts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/mimetype:
--------------------------------------------------------------------------------
1 | application/epub+zip
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/dist
3 | **/.next
4 | **/.turbo
5 |
--------------------------------------------------------------------------------
/apps/reader/src/components/pages/index.ts:
--------------------------------------------------------------------------------
1 | export * from './settings'
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "apps/*"
3 | - "packages/*"
4 |
--------------------------------------------------------------------------------
/packages/epubjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.ts.json"
3 | }
4 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/value_objects/__init__.py:
--------------------------------------------------------------------------------
1 | """アノテーション値オブジェクトモジュール"""
2 |
--------------------------------------------------------------------------------
/apps/reader/src/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './reader'
2 | export * from './tree'
3 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | pnpm lint-staged
5 |
--------------------------------------------------------------------------------
/apps/reader/src/pages/_.tsx:
--------------------------------------------------------------------------------
1 | import Index from './index'
2 |
3 | export default Index
4 |
--------------------------------------------------------------------------------
/packages/epubjs/types/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "dtslint/dt.json",
3 | "rules": {}
4 | }
5 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/entities/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.book.entities.book import Book as Book
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # https://github.com/remix-run/remix/issues/154#issuecomment-978359765
2 | # shamefully-hoist=true
3 |
--------------------------------------------------------------------------------
/packages/epubjs/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": [
3 | ".git",
4 | "node_modules"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/apps/reader/sentry.properties:
--------------------------------------------------------------------------------
1 | defaults.url=https://sentry.io/
2 | defaults.org=pacexy
3 | defaults.project=flow
4 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/external/epub/__init__.py:
--------------------------------------------------------------------------------
1 | from .epub_reader import Chapter
2 |
3 | __all__ = ["Chapter"]
4 |
--------------------------------------------------------------------------------
/apps/reader/public/icons/192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/apps/reader/public/icons/192.png
--------------------------------------------------------------------------------
/apps/reader/public/icons/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/apps/reader/public/icons/512.png
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
5 | # dotenvx
6 | .env.keys
7 | .env.local
8 | .env.*.local
9 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/di/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.di.injection import get_book_repository as get_book_repository
2 |
--------------------------------------------------------------------------------
/apps/reader/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/external/gemini/__init__.py:
--------------------------------------------------------------------------------
1 | from .gemini_client import GeminiClient
2 |
3 | __all__ = ["GeminiClient"]
4 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/__init__.py:
--------------------------------------------------------------------------------
1 | from src.presentation.api.routes import setup_routes
2 |
3 | __all__ = ["setup_routes"]
4 |
--------------------------------------------------------------------------------
/apps/reader/public/icons/maskable-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/apps/reader/public/icons/maskable-192.png
--------------------------------------------------------------------------------
/apps/reader/public/icons/maskable-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/apps/reader/public/icons/maskable-512.png
--------------------------------------------------------------------------------
/packages/epubjs/documentation.yml:
--------------------------------------------------------------------------------
1 | toc:
2 | - ePub
3 | - name: ePubJS
4 | description: |
5 | main entry
6 | - EpubCFI
7 |
--------------------------------------------------------------------------------
/packages/epubjs/examples/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/examples/ajax-loader.gif
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice.epub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice.epub
--------------------------------------------------------------------------------
/apps/api/src/domain/message/entities/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.message.entities.message import Message
2 |
3 | __all__ = ["Message"]
4 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/external/audio/__init__.py:
--------------------------------------------------------------------------------
1 | from .audio_processor import AudioProcessor
2 |
3 | __all__ = ["AudioProcessor"]
4 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/scrolltype.d.ts:
--------------------------------------------------------------------------------
1 | export default function scrollType(): string
2 |
3 | export function createDefiner(): Node
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 | **/pnpm-lock.yaml
3 | # ビルド生成物を無視
4 | .next
5 | **/.next
6 | build
7 | **/build
8 | dist
9 | **/dist
10 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/chat/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.postgres.chat.chat_dto import ChatDTO
2 |
3 | __all__ = ["ChatDTO"]
4 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/user/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.postgres.user.user_dto import UserDTO
2 |
3 | __all__ = ["UserDTO"]
4 |
--------------------------------------------------------------------------------
/apps/reader/.prettierignore:
--------------------------------------------------------------------------------
1 | .next
2 | **/.next
3 | build
4 | **/build
5 | dist
6 | **/dist
7 | pnpm-lock.yaml
8 | **/pnpm-lock.yaml
9 |
10 |
--------------------------------------------------------------------------------
/apps/reader/src/components/Reader.tsx:
--------------------------------------------------------------------------------
1 | // Re-export the main ReaderGridView component
2 | export { ReaderGridView } from './reader/ReaderGridView'
3 |
--------------------------------------------------------------------------------
/supabase/migrations/20250713003643_add_language_to_podcast.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."podcasts" add column "language" character varying(10) not null;
2 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/repositories/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.book.repositories.book_repository import BookRepository
2 |
3 | __all__ = ["BookRepository"]
4 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/podcast/__init__.py:
--------------------------------------------------------------------------------
1 | from .podcast_repository import PodcastRepositoryImpl
2 |
3 | __all__ = ["PodcastRepositoryImpl"]
4 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice_without_cover.epub:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice_without_cover.epub
--------------------------------------------------------------------------------
/packages/internal/src/lib.ts:
--------------------------------------------------------------------------------
1 | export const str = 'Hello world'
2 |
3 | export function range(n: number) {
4 | return [...new Array(n)].map((_, i) => i)
5 | }
6 |
--------------------------------------------------------------------------------
/apps/reader/src/components/Library.tsx:
--------------------------------------------------------------------------------
1 | // Re-export the main LibraryContainer component
2 | export { LibraryContainer as Library } from './library/LibraryContainer'
3 |
--------------------------------------------------------------------------------
/apps/reader/src/components/base/GridView.tsx:
--------------------------------------------------------------------------------
1 | import React, { JSX } from 'react'
2 |
3 | export const GridView = (): JSX.Element => {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/apps/reader/src/types/window.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Window {
3 | podcastSeekFunction?: (time: number) => void
4 | }
5 | }
6 |
7 | export {}
8 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/title.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/title.jpg
--------------------------------------------------------------------------------
/packages/internal/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.react.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/api/src/__init__.py:
--------------------------------------------------------------------------------
1 | from src.config.db import Base, SessionLocal, engine, get_db, init_db
2 |
3 | __all__ = ["Base", "get_db", "init_db", "engine", "SessionLocal"]
4 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/cover_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/cover_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i001_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i001_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i002_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i002_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i003_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i003_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i004_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i004_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i005_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i005_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i006_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i006_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i007_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i007_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i008_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i008_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i009_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i009_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i010_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i010_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i011_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i011_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i012_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i012_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i013_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i013_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i014_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i014_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i015_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i015_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i016_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i016_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i017_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i017_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i018_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i018_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i019_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i019_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i020_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i020_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/i022_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/i022_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/ii021_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/ii021_th.jpg
--------------------------------------------------------------------------------
/apps/api/src/domain/message/repositories/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.message.repositories.message_repository import MessageRepository
2 |
3 | __all__ = ["MessageRepository"]
4 |
--------------------------------------------------------------------------------
/apps/reader/src/components/TextSelectionMenu.tsx:
--------------------------------------------------------------------------------
1 | // Re-export the main TextSelectionMenu component
2 | export { TextSelectionMenu } from './textSelection/TextSelectionMenu'
3 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useBackground'
2 | export * from './useColorScheme'
3 | export * from './useSourceColor'
4 | export * from './useTheme'
5 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/plate01_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/plate01_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/plate02_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/plate02_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/plate03_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/plate03_th.jpg
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/images/plate04_th.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shutootaki/bookwith/HEAD/packages/epubjs/test/fixtures/alice/OPS/images/plate04_th.jpg
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/annotation/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.postgres.annotation.annotation_dto import AnnotationDTO
2 |
3 | __all__ = ["AnnotationDTO"]
4 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/chat/__init__.py:
--------------------------------------------------------------------------------
1 | from src.config.db import Base, SessionLocal, engine, get_db, init_db
2 |
3 | __all__ = ["Base", "get_db", "init_db", "engine", "SessionLocal"]
4 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('prettier-plugin-tailwindcss')],
3 | singleQuote: true,
4 | semi: false,
5 | trailingComma: 'all',
6 | }
7 |
--------------------------------------------------------------------------------
/packages/epubjs/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | */**/.DS_Store
3 | node_modules/
4 | components
5 | node_modules
6 | bower_components
7 | books
8 | lib
9 | dist
10 | documentation/html
11 | types/*.js
--------------------------------------------------------------------------------
/packages/epubjs/types/container.d.ts:
--------------------------------------------------------------------------------
1 | export default class Container {
2 | constructor(containerDocument: Document)
3 |
4 | parse(containerDocument: Document): void
5 |
6 | destroy(): void
7 | }
8 |
--------------------------------------------------------------------------------
/.mcp.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "playwright": {
4 | "type": "stdio",
5 | "command": "npx",
6 | "args": ["-y", "@playwright/mcp@latest"],
7 | "env": {}
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/reader/src/components/base/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ActionBar'
2 | export * from './ContextView'
3 | export * from './DropZone'
4 | export * from './GridView'
5 | export * from './PaneView'
6 | export * from './SplitView'
7 |
--------------------------------------------------------------------------------
/packages/tailwind/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@flow/tailwind",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "main": "./src/index.js",
6 | "devDependencies": {
7 | "tailwindcss": "^3.2.0"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/loading/index.ts:
--------------------------------------------------------------------------------
1 | export { useLoadingState } from './useLoadingState'
2 | export { useProgressManager } from './useProgressManager'
3 | export { useTaskManager, type UseTaskManagerOptions } from './useTaskManager'
4 |
--------------------------------------------------------------------------------
/apps/reader/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import { ClassValue } from 'clsx'
3 | import { twMerge } from 'tailwind-merge'
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/request.d.ts:
--------------------------------------------------------------------------------
1 | export default function request(
2 | url: string,
3 | type?: string,
4 | withCredentials?: boolean,
5 | headers?: object,
6 | ): Promise
7 |
--------------------------------------------------------------------------------
/packages/epubjs/examples/themes.css:
--------------------------------------------------------------------------------
1 | .dark {
2 | background: #000;
3 | color: #fff;
4 | }
5 |
6 | .light {
7 | background: #fff;
8 | color: #000;
9 | }
10 |
11 | .tan {
12 | background: tan;
13 | color: #ccc;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/constants.d.ts:
--------------------------------------------------------------------------------
1 | export const EPUBJS_VERSION: string
2 |
3 | export const DOM_EVENTS: Array
4 |
5 | export const EVENTS: {
6 | [key: string]: {
7 | [key: string]: string
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/reader/locales/index.ts:
--------------------------------------------------------------------------------
1 | import cmn_CN from './cmn-CN'
2 | import en_US from './en-US'
3 | import ja_JP from './ja-JP'
4 |
5 | export default {
6 | 'en-US': en_US,
7 | 'ja-JP': ja_JP,
8 | 'cmn-CN': cmn_CN,
9 | } as const
10 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/book/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.postgres.book.book_dto import BookDTO
2 | from src.infrastructure.postgres.book.book_repository import BookRepositoryImpl
3 |
4 | __all__ = ["BookDTO", "BookRepositoryImpl"]
5 |
--------------------------------------------------------------------------------
/apps/reader/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useForceRender.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react'
2 |
3 | export function useForceRender() {
4 | const [, render] = useState({})
5 |
6 | return useCallback(() => {
7 | render({})
8 | }, [])
9 | }
10 |
--------------------------------------------------------------------------------
/apps/reader/src/types/podcast.ts:
--------------------------------------------------------------------------------
1 | import { components } from '../lib/openapi-schema/schema'
2 |
3 | export type PodcastResponse = components['schemas']['PodcastResponse']
4 |
5 | export type PodcastStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED'
6 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/mime.ts:
--------------------------------------------------------------------------------
1 | export const mapExtToMimes = {
2 | '.epub': ['application/epub+zip', 'application/epub'],
3 | '.zip': [
4 | 'application/zip',
5 | 'application/zip-compressed',
6 | 'application/x-zip-compressed',
7 | ],
8 | }
9 |
--------------------------------------------------------------------------------
/apps/reader/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.next.json",
3 | "compilerOptions": {
4 | "experimentalDecorators": true
5 | },
6 | "include": ["next-env.d.ts", "web.d.ts", "**/*.ts", "**/*.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/epubjs/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 'node'
4 | notifications:
5 | email: false
6 | sudo: false
7 | addons:
8 | chrome: stable
9 | cache:
10 | directories:
11 | - node_modules
12 | script:
13 | - npm test
14 |
--------------------------------------------------------------------------------
/apps/reader/src/store/loading/atoms.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai'
2 |
3 | import { LoadingTaskMap } from '../../types/loading'
4 |
5 | /**
6 | * Core atom that holds all loading tasks
7 | */
8 | export const loadingTasksAtom = atom(new Map())
9 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/value_objects/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.message.value_objects.message_content import MessageContent
2 | from src.domain.message.value_objects.message_id import MessageId
3 |
4 | __all__ = [
5 | "MessageId",
6 | "MessageContent",
7 | ]
8 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/memory/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.memory.memory_service import MemoryService
2 | from src.infrastructure.memory.memory_vector_store import MemoryVectorStore
3 |
4 | __all__ = [
5 | "MemoryService",
6 | "MemoryVectorStore",
7 | ]
8 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/message/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.postgres.message.message_dto import MessageDTO
2 | from src.infrastructure.postgres.message.message_repository import MessageRepositoryImpl
3 |
4 | __all__ = ["MessageDTO", "MessageRepositoryImpl"]
5 |
--------------------------------------------------------------------------------
/apps/reader/src/components/library/index.ts:
--------------------------------------------------------------------------------
1 | export { LibraryContainer } from './LibraryContainer'
2 | export { RemoteImportManager as ImportManager } from './RemoteImportManager'
3 | export { SelectionManager } from './SelectionManager'
4 | export { BookGrid } from './BookGrid'
5 |
--------------------------------------------------------------------------------
/apps/reader/src/components/textSelection/index.ts:
--------------------------------------------------------------------------------
1 | export { TextSelectionMenu } from './TextSelectionMenu'
2 | export { TextSelectionRenderer } from './TextSelectionRenderer'
3 | export { ActionMenu } from './ActionMenu'
4 | export { AnnotationToolbar } from './AnnotationToolbar'
5 |
--------------------------------------------------------------------------------
/apps/reader/src/components/chat/types.ts:
--------------------------------------------------------------------------------
1 | import { components } from '../../lib/openapi-schema/schema'
2 |
3 | export interface Message {
4 | text: components['schemas']['MessageResponse']['content']
5 | senderType: components['schemas']['MessageResponse']['senderType']
6 | }
7 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useEnv.ts:
--------------------------------------------------------------------------------
1 | import { useMobile } from './useMobile'
2 |
3 | export enum Env {
4 | Desktop = 1,
5 | Mobile = 1 << 1,
6 | }
7 |
8 | export function useEnv() {
9 | const mobile = useMobile()
10 | return mobile ? Env.Mobile : Env.Desktop
11 | }
12 |
--------------------------------------------------------------------------------
/packages/epubjs/types/epub.d.ts:
--------------------------------------------------------------------------------
1 | import Book, { BookOptions } from './book'
2 |
3 | export default Epub
4 |
5 | declare function Epub(
6 | urlOrData: string | ArrayBuffer,
7 | options?: BookOptions,
8 | ): Book
9 | declare function Epub(options?: BookOptions): Book
10 |
--------------------------------------------------------------------------------
/tsconfig.ts.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "TypeScript Library",
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "lib": ["esnext"],
7 | "module": "ESNext",
8 | "target": "ES6"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
2 | package = []
3 |
4 | [metadata]
5 | lock-version = "2.1"
6 | python-versions = ">=3.11,<4.0"
7 | content-hash = "6e476a2a110ac6b4d38bba4321a4054bf79bf7c0f098f4cda47c09e95c0b73b2"
8 |
--------------------------------------------------------------------------------
/apps/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@flow/api",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "docker:up": "docker compose up --build --pull always",
7 | "dev": "make run",
8 | "dev:services": "make docker.up",
9 | "lint": "make lint"
10 | }
11 | }
--------------------------------------------------------------------------------
/packages/epubjs/types/epubjs-tests.ts:
--------------------------------------------------------------------------------
1 | import ePub, { Book } from '../'
2 |
3 | function testEpub() {
4 | const epub = ePub('https://s3.amazonaws.com/moby-dick/moby-dick.epub')
5 |
6 | const book = new Book('https://s3.amazonaws.com/moby-dick/moby-dick.epub', {})
7 | }
8 |
9 | testEpub()
10 |
--------------------------------------------------------------------------------
/packages/internal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@flow/internal",
3 | "version": "0.0.0",
4 | "main": "./src/index.ts",
5 | "types": "./src/index.ts",
6 | "license": "MIT",
7 | "devDependencies": {
8 | "@types/react": "17.0.43",
9 | "@types/react-dom": "18.0.0"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/reader/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | reader:
5 | container_name: reader
6 | build:
7 | context: .
8 | dockerfile: ./Dockerfile
9 | restart: always
10 | ports:
11 | - 3000:3000
12 | env_file:
13 | - ./apps/reader/.env.local
14 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/META-INF/container.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/url.d.ts:
--------------------------------------------------------------------------------
1 | import Path from './path'
2 |
3 | export default class Url {
4 | constructor(urlString: string, baseString: string)
5 |
6 | path(): Path
7 |
8 | resolve(what: string): string
9 |
10 | relative(what: string): string
11 |
12 | toString(): string
13 | }
14 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/exceptions.py:
--------------------------------------------------------------------------------
1 | class AnnotationNotFoundError(Exception):
2 | """指定されたアノテーションが見つからない場合に送出される例外"""
3 |
4 | def __init__(self, annotation_id: str) -> None:
5 | self.annotation_id = annotation_id
6 | super().__init__(f"Annotation with id '{annotation_id}' not found.")
7 |
--------------------------------------------------------------------------------
/packages/epubjs/src/index.js:
--------------------------------------------------------------------------------
1 | import Book from './book'
2 | import Contents from './contents'
3 | import ePub from './epub'
4 | import EpubCFI from './epubcfi'
5 | import Layout from './layout'
6 | import Rendition from './rendition'
7 |
8 | export default ePub
9 | export { Book, EpubCFI, Rendition, Contents, Layout }
10 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/value_objects/message_content.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass(frozen=True)
5 | class MessageContent:
6 | value: str
7 |
8 | def __post_init__(self) -> None:
9 | if not self.value:
10 | raise ValueError("Message content cannot be empty")
11 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/value_objects/language.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class PodcastLanguage(str, Enum):
5 | EN_US = "en-US"
6 | JA_JP = "ja-JP"
7 | CMN_CN = "cmn-CN"
8 |
9 | @classmethod
10 | def has_value(cls, value: str) -> bool:
11 | return value in cls._value2member_map_
12 |
--------------------------------------------------------------------------------
/tsconfig.react.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "lib": ["dom", "dom.iterable", "esnext"],
7 | "module": "ESNext",
8 | "target": "ES6",
9 | "jsx": "react-jsx"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "build": {
4 | "dependsOn": ["^build"],
5 | "outputs": ["dist/**", ".next/**"]
6 | },
7 | "lint": {
8 | "outputs": []
9 | },
10 | "dev": {},
11 | "dev:services": {
12 | "cache": false,
13 | "persistent": true
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/repositories/annotation_repository.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.book.entities.book import Book
4 |
5 |
6 | class AnnotationRepository(ABC):
7 | @abstractmethod
8 | def sync_annotations(self, book: Book) -> None:
9 | """書籍に関連する全てのアノテーションを同期する"""
10 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/schemas/base_schema.py:
--------------------------------------------------------------------------------
1 | import humps
2 | from pydantic import BaseModel, ConfigDict
3 |
4 |
5 | def to_camel(string: str) -> str:
6 | return humps.camelize(string)
7 |
8 |
9 | class BaseSchemaModel(BaseModel):
10 | model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
11 |
--------------------------------------------------------------------------------
/apps/reader/.env.example:
--------------------------------------------------------------------------------
1 | # For develop features related to synchronization,
2 | # you should register an app on Dropbox:
3 | # https://www.dropbox.com/developers/apps/create
4 | NEXT_PUBLIC_DROPBOX_CLIENT_ID=
5 | DROPBOX_CLIENT_SECRET=
6 |
7 | NEXT_PUBLIC_WEBSITE_URL=http://localhost:7117
8 | NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
9 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/value_objects/chat_title.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass(frozen=True)
5 | class ChatTitle:
6 | value: str
7 |
8 | def __post_init__(self) -> None:
9 | if self.value and len(self.value) > 255:
10 | raise ValueError("Chat title must be 255 characters or less")
11 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/platform.ts:
--------------------------------------------------------------------------------
1 | import { IS_SERVER } from './utils'
2 |
3 | // https://www.geeksforgeeks.org/how-to-detect-touch-screen-device-using-javascript
4 | export const isTouchScreen = IS_SERVER ? false : 'ontouchstart' in window
5 | export const scale = (value: number, valueInTouchScreen: number) =>
6 | isTouchScreen ? valueInTouchScreen : value
7 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useList.ts:
--------------------------------------------------------------------------------
1 | import useVirtual from 'react-cool-virtual'
2 |
3 | import { scale } from '../utils/platform'
4 |
5 | export const LIST_ITEM_SIZE = scale(24, 32)
6 | export function useList(array: Readonly = []) {
7 | return useVirtual({
8 | itemCount: array.length,
9 | itemSize: LIST_ITEM_SIZE,
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "bookwith-monorepo"
3 | version = "0.1.0"
4 | description = "Monorepo for bookwith project"
5 | authors = []
6 |
7 | [tool.poetry.dependencies]
8 | python = ">=3.11,<4.0"
9 |
10 | [tool.poetry.group.dev.dependencies]
11 |
12 | [build-system]
13 | requires = ["poetry-core>=1.7.0"]
14 | build-backend = "poetry.core.masonry.api"
15 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.message.exceptions.message_exceptions import (
2 | MessageAlreadyDeletedException,
3 | MessageDeliveryFailedException,
4 | MessageNotFoundException,
5 | )
6 |
7 | __all__ = [
8 | "MessageNotFoundException",
9 | "MessageAlreadyDeletedException",
10 | "MessageDeliveryFailedException",
11 | ]
12 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/value_objects/book_title.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass(frozen=True)
5 | class BookTitle:
6 | value: str
7 |
8 | def __post_init__(self) -> None:
9 | if not self.value:
10 | raise ValueError("タイトルは必須です")
11 | if len(self.value) > 100:
12 | raise ValueError("タイトルは100文字以下である必要があります")
13 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/theme/useTheme.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from '@material/material-color-utilities'
2 | import { atom, useAtomValue, useSetAtom } from 'jotai'
3 |
4 | const themeState = atom(undefined)
5 |
6 | export function useTheme() {
7 | return useAtomValue(themeState)
8 | }
9 |
10 | export function useSetTheme() {
11 | return useSetAtom(themeState)
12 | }
13 |
--------------------------------------------------------------------------------
/packages/epubjs/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from '../../eslint.config.js';
2 | import tseslint from 'typescript-eslint';
3 |
4 | export default tseslint.config(
5 | ...baseConfig,
6 | {
7 | files: ['**/*.ts', '**/*.tsx'],
8 | rules: {
9 | '@typescript-eslint/ban-types': 'off',
10 | '@typescript-eslint/no-unused-vars': 'off',
11 | },
12 | }
13 | );
14 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useAfterMount.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | function useMounted() {
4 | const [mounted, setMounted] = useState(false)
5 | useEffect(() => {
6 | setMounted(true)
7 | return () => setMounted(false)
8 | }, [])
9 | return mounted
10 | }
11 |
12 | export function useAfterMount(v: T) {
13 | return useMounted() ? v : null
14 | }
15 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/error_messages/chat_error_message.py:
--------------------------------------------------------------------------------
1 | class ChatErrorMessage:
2 | CHAT_NOT_FOUND = "チャットが見つかりません"
3 | CHAT_ALREADY_EXISTS = "チャットは既に存在します"
4 | CHAT_VALIDATION_ERROR = "チャットのバリデーションエラーが発生しました"
5 | CHAT_TITLE_TOO_LONG = "チャットタイトルは255文字以内である必要があります"
6 | CHAT_ID_INVALID = "無効なチャットIDです"
7 | USER_ID_INVALID = "無効なユーザーIDです"
8 | BOOK_ID_INVALID = "無効な本IDです"
9 |
--------------------------------------------------------------------------------
/apps/reader/src/store/loading.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Loading store - backward compatibility layer
3 | *
4 | * This file re-exports from the new modular structure to maintain
5 | * backward compatibility with existing imports.
6 | *
7 | * @deprecated Consider importing from '@/store/loading' instead
8 | */
9 |
10 | // Re-export everything from the new modular structure
11 | export * from './loading/index'
12 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/path.d.ts:
--------------------------------------------------------------------------------
1 | export default class Path {
2 | constructor(pathString: string)
3 |
4 | parse(what: string): object
5 |
6 | isAbsolute(what: string): boolean
7 |
8 | isDirectory(what: string): boolean
9 |
10 | resolve(what: string): string
11 |
12 | relative(what: string): string
13 |
14 | splitPath(filename: string): string
15 |
16 | toString(): string
17 | }
18 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/hook.d.ts:
--------------------------------------------------------------------------------
1 | interface HooksObject {
2 | [key: string]: Hook
3 | }
4 |
5 | export default class Hook {
6 | constructor(context: any)
7 |
8 | register(func: Function): void
9 | register(arr: Array): void
10 |
11 | deregister(func: Function): void
12 |
13 | trigger(...args: any[]): Promise
14 |
15 | list(): Array
16 |
17 | clear(): void
18 | }
19 |
--------------------------------------------------------------------------------
/apps/reader/src/components/reader/PaneContainer.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import React, { FC, PropsWithChildren } from 'react'
3 |
4 | interface PaneContainerProps {
5 | active: boolean
6 | }
7 |
8 | export const PaneContainer: FC> = ({
9 | active,
10 | children,
11 | }) => {
12 | return {children}
13 | }
14 |
--------------------------------------------------------------------------------
/apps/reader/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Annotation'
2 | export * from './base'
3 | export * from './Button'
4 | export * from './ErrorBoundary'
5 | export * from './Form'
6 | export * from './Layout'
7 | export * from './Page'
8 | export * from './pages'
9 | export * from './Reader'
10 | export * from './Row'
11 | export * from './Tab'
12 | export * from './TextSelectionMenu'
13 | export * from './Theme'
14 |
--------------------------------------------------------------------------------
/apps/reader/src/components/reader/index.ts:
--------------------------------------------------------------------------------
1 | export { ReaderGridView } from './ReaderGridView'
2 | export { ReaderGroup } from './ReaderGroup'
3 | export { BookPane } from './BookPane'
4 | export { PaneContainer } from './PaneContainer'
5 | export { ReaderPaneHeader } from './ReaderPaneHeader'
6 | export { ReaderPaneFooter } from './ReaderPaneFooter'
7 | export { Bar } from './Bar'
8 | export { handleKeyDown } from './eventHandlers'
9 |
--------------------------------------------------------------------------------
/apps/reader/src/components/viewlets/PodcastView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { PaneView, PaneViewProps } from '../base'
4 | import { PodcastPane } from '../podcast'
5 |
6 | export const PodcastView: React.FC = (props) => {
7 | return (
8 |
9 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/reader/web.d.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/christianliebel/paint/blob/850a57cd3cc6f6532791abb6d20d9228ceffb74f/types/static.d.ts#L66
2 | // Type declarations for File Handling API
3 |
4 | interface LaunchParams {
5 | files: FileSystemFileHandle[]
6 | }
7 |
8 | interface LaunchQueue {
9 | setConsumer(consumer: (launchParams: LaunchParams) => any): void
10 | }
11 |
12 | interface Window {
13 | launchQueue: LaunchQueue
14 | }
15 |
--------------------------------------------------------------------------------
/packages/epubjs/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": "last 2 Chrome versions, last 2 Safari versions, last 2 ChromeAndroid versions, last 2 iOS versions, last 2 Firefox versions, last 2 Edge versions",
7 | "corejs": 3,
8 | "useBuiltIns": "usage",
9 | "bugfixes": true,
10 | "modules": "auto"
11 | }
12 | ]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/epubjs/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Mocha
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useAutoResize.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export const useAutoResize = (
4 | text: string,
5 | textareaRef: {
6 | current: HTMLTextAreaElement | null
7 | },
8 | ) => {
9 | useEffect(() => {
10 | const textarea = textareaRef.current
11 | if (!textarea) return
12 | textarea.style.height = 'auto'
13 | textarea.style.height = `${textarea.scrollHeight}px`
14 | }, [text, textareaRef])
15 | }
16 |
--------------------------------------------------------------------------------
/apps/reader/src/components/reader/Bar.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import React, { ComponentProps, FC } from 'react'
3 |
4 | export const Bar: FC> = ({ className, ...props }) => {
5 | return (
6 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/packages/epubjs/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "browser": true,
3 | "devel": true,
4 | "worker": true,
5 |
6 | "trailing": true,
7 | "strict": false,
8 |
9 | "boss": true,
10 | "funcscope": true,
11 | "globalstrict": true,
12 | "loopfunc": true,
13 | "maxerr": 1000,
14 | "nonstandard": true,
15 | "sub": true,
16 | "validthis": true,
17 |
18 | "globals": {
19 | "_": false,
20 | "define" : false,
21 | "module" : false
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/cover.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cover
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/value_objects/book_id.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from uuid import UUID
3 |
4 |
5 | @dataclass(frozen=True)
6 | class BookId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | raise ValueError("Book ID is required")
12 | try:
13 | UUID(self.value)
14 | except ValueError:
15 | raise ValueError("Book ID must be a valid UUID")
16 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/value_objects/chat_id.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from uuid import UUID
3 |
4 |
5 | @dataclass(frozen=True)
6 | class ChatId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | raise ValueError("Chat ID is required")
12 | try:
13 | UUID(self.value)
14 | except ValueError:
15 | raise ValueError("Chat ID must be a valid UUID")
16 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/value_objects/user_id.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from uuid import UUID
3 |
4 |
5 | @dataclass(frozen=True)
6 | class UserId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | raise ValueError("User ID is required")
12 | try:
13 | UUID(self.value)
14 | except ValueError:
15 | raise ValueError("User ID must be a valid UUID")
16 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/usePress.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | import { useEventListener } from './useEventListener'
4 |
5 | export function usePress(target: React.RefObject | null) {
6 | const [pressed, setPressed] = useState(false)
7 | useEventListener(target?.current, 'mousedown', () => {
8 | setPressed(true)
9 | })
10 | useEventListener('mouseup', () => {
11 | setPressed(false)
12 | })
13 | return pressed
14 | }
15 |
--------------------------------------------------------------------------------
/packages/epubjs/test/old/rendering.js:
--------------------------------------------------------------------------------
1 | module('Rendering')
2 | /*
3 | asyncTest("Render To", 1, function() {
4 |
5 | var book = ePub("../books/moby-dick/OPS/package.opf");
6 | var rendition = book.renderTo("qunit-fixture", {width:400, height:600});
7 | var displayed = rendition.display(0);
8 |
9 | displayed.then(function(){
10 | equal( $( "iframe", "#qunit-fixture" ).length, 1, "iframe added successfully" );
11 | start();
12 | });
13 |
14 |
15 | });
16 | */
17 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/__init__.py:
--------------------------------------------------------------------------------
1 | from src.infrastructure.postgres.annotation.annotation_dto import AnnotationDTO
2 | from src.infrastructure.postgres.book.book_dto import BookDTO
3 | from src.infrastructure.postgres.chat.chat_dto import ChatDTO
4 | from src.infrastructure.postgres.message.message_dto import MessageDTO
5 | from src.infrastructure.postgres.user.user_dto import UserDTO
6 |
7 | __all__ = ["UserDTO", "BookDTO", "AnnotationDTO", "ChatDTO", "MessageDTO"]
8 |
--------------------------------------------------------------------------------
/apps/reader/netlify.toml:
--------------------------------------------------------------------------------
1 | [build.environment]
2 | NODE_VERSION = "16"
3 | # https://github.com/netlify/build/issues/1633#issuecomment-907246600
4 | NPM_FLAGS = "--version" # prevent Netlify npm install
5 |
6 | [build]
7 | # Set `base` to repo directory in Netlify UI
8 | # base = 'reader'
9 | publish = ".next"
10 | # https://answers.netlify.com/t/using-pnpm-and-pnpm-workspaces/2759
11 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm -F reader... build"
12 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useAsync.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 |
3 | export function useAsync(
4 | func: () => Promise | undefined | null,
5 | deps = [],
6 | ) {
7 | const ref = useRef(func)
8 | ref.current = func
9 | const [value, setValue] = useState()
10 |
11 | useEffect(() => {
12 | ref.current()?.then(setValue)
13 | // eslint-disable-next-line react-hooks/exhaustive-deps
14 | }, deps)
15 |
16 | return value
17 | }
18 |
--------------------------------------------------------------------------------
/packages/epubjs/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "lib": ["es6", "dom"],
5 | "noImplicitAny": true,
6 | "noImplicitThis": true,
7 | "strictNullChecks": true,
8 | "strictFunctionTypes": true,
9 | "baseUrl": "../",
10 | "typeRoots": ["../"],
11 | "types": [],
12 | "noEmit": true,
13 | "forceConsistentCasingInFileNames": true
14 | },
15 | "files": ["index.d.ts", "epubjs-tests.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useBoolean.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 |
3 | export function useBoolean(
4 | initial?: boolean,
5 | ): readonly [boolean | undefined, (v?: any) => void] {
6 | const [state, setState] = useState(initial)
7 | const toggle = useCallback((v?: any) => {
8 | setState((s) => {
9 | if (typeof v === 'boolean') {
10 | return v
11 | }
12 | return !s
13 | })
14 | }, [])
15 | return [state, toggle]
16 | }
17 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useHover.ts:
--------------------------------------------------------------------------------
1 | import { useState, RefObject } from 'react'
2 |
3 | import { useEventListener } from './useEventListener'
4 |
5 | export function useHover(target: RefObject | null) {
6 | const [hovered, setHovered] = useState(false)
7 | useEventListener(target?.current, 'mouseenter', () => {
8 | setHovered(true)
9 | })
10 | useEventListener(target?.current, 'mouseleave', () => {
11 | setHovered(false)
12 | })
13 | return hovered
14 | }
15 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/error_messages/message_error_message.py:
--------------------------------------------------------------------------------
1 | class MessageErrorMessage:
2 | MESSAGE_NOT_FOUND = "メッセージが見つかりません。"
3 | MESSAGE_ALREADY_DELETED = "既に削除されたメッセージです。"
4 | MESSAGE_ALREADY_READ = "既に既読のメッセージです。"
5 | MESSAGE_DELIVERY_FAILED = "メッセージの配信に失敗しました。"
6 | MESSAGE_PERMISSION_DENIED = "このメッセージへのアクセス権限がありません。"
7 | MESSAGE_CREATE_FAILED = "メッセージの作成に失敗しました。"
8 | MESSAGE_UPDATE_FAILED = "メッセージの更新に失敗しました。"
9 | MESSAGE_DELETE_FAILED = "メッセージの削除に失敗しました。"
10 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/charliermarsh/ruff-pre-commit
3 | rev: v0.11.5
4 | hooks:
5 | - id: ruff
6 | args: [--fix, --unsafe-fixes]
7 | - id: ruff-format
8 | - repo: https://github.com/python-poetry/poetry
9 | rev: 2.1.2
10 | hooks:
11 | - id: poetry-check
12 | - id: poetry-lock
13 | files: ^(pyproject\.toml|poetry\.lock)$
14 | default_language_version:
15 | python: python3
16 | minimum_pre_commit_version: 4.0.1
17 |
--------------------------------------------------------------------------------
/apps/reader/src/components/podcast/index.ts:
--------------------------------------------------------------------------------
1 | export { AudioPlayer } from './AudioPlayer'
2 | export { AudioControls } from './AudioControls'
3 | export { VolumeControl } from './VolumeControl'
4 | export { SpeedControl } from './SpeedControl'
5 | export { BookPodcastItem } from './BookPodcastItem'
6 | export { LibraryPodcastView } from './LibraryPodcastView'
7 | export { PodcastDetail } from './PodcastDetail'
8 | export { PodcastPane } from './PodcastPane'
9 | export { PodcastScript } from './PodcastScript'
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: 🚀 Feature Request
2 | description: Suggest an idea, feature, or enhancement
3 | labels: [enhancement]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for submitting your idea! It helps make BookWith better.
9 |
10 | - type: textarea
11 | attributes:
12 | label: What feature would you like to see?
13 | description: Please describe what you'd like to happen.
14 | validations:
15 | required: true
16 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useAction.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom, useSetAtom } from 'jotai'
2 |
3 | export type Action =
4 | | 'toc'
5 | | 'chat'
6 | | 'search'
7 | | 'annotation'
8 | | 'typography'
9 | | 'image'
10 | | 'timeline'
11 | | 'theme'
12 | | 'podcast'
13 |
14 | export const actionState = atom(undefined)
15 |
16 | export function useSetAction() {
17 | return useSetAtom(actionState)
18 | }
19 |
20 | export function useAction() {
21 | return useAtom(actionState)
22 | }
23 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/value_objects/annotation_cfi.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass(frozen=True)
5 | class AnnotationCfi:
6 | value: str
7 |
8 | def __post_init__(self) -> None:
9 | if not self.value:
10 | raise ValueError("CFI is required")
11 | if not isinstance(self.value, str):
12 | raise ValueError("CFI must be a string")
13 |
14 | @classmethod
15 | def from_string(cls, cfi_str: str) -> "AnnotationCfi":
16 | return cls(cfi_str)
17 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/value_objects/annotation_notes.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass(frozen=True)
5 | class AnnotationNotes:
6 | value: str | None
7 |
8 | def __post_init__(self) -> None:
9 | if self.value is not None and not isinstance(self.value, str):
10 | raise ValueError("Annotation notes must be a string")
11 |
12 | @classmethod
13 | def from_string(cls, notes_str: str | None) -> "AnnotationNotes":
14 | """文字列からAnnotationNotesを生成"""
15 | return cls(notes_str)
16 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/replacements.d.ts:
--------------------------------------------------------------------------------
1 | import Contents from '../contents'
2 | import Section from '../section'
3 |
4 | export function replaceBase(doc: Document, section: Section): void
5 |
6 | export function replaceCanonical(doc: Document, section: Section): void
7 |
8 | export function replaceMeta(doc: Document, section: Section): void
9 |
10 | export function replaceLinks(contents: Contents, fn: Function): void
11 |
12 | export function substitute(
13 | contents: Contents,
14 | urls: string[],
15 | replacements: string[],
16 | ): void
17 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useSWR/fetcher.ts:
--------------------------------------------------------------------------------
1 | type SWRError = {
2 | status: number
3 | } & Error
4 |
5 | export async function fetcher(
6 | input: RequestInfo,
7 | init?: RequestInit,
8 | ): Promise {
9 | const res = await fetch(input, {
10 | ...init,
11 | headers: {
12 | ...init?.headers,
13 | },
14 | })
15 |
16 | if (!res.ok) {
17 | const error = await res.text()
18 | const err = new Error(error) as SWRError
19 | err.status = res.status
20 | throw err
21 | }
22 |
23 | return await res.json()
24 | }
25 |
--------------------------------------------------------------------------------
/apps/reader/src/store/loading/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Loading store - modular structure
3 | *
4 | * This file re-exports all loading-related atoms and utilities
5 | * to maintain backward compatibility while providing a clean modular structure.
6 | */
7 |
8 | // Type definitions
9 | export * from '../../types/loading'
10 |
11 | // Core atoms
12 | export * from './atoms'
13 |
14 | // Selectors (derived atoms)
15 | export * from './selectors'
16 |
17 | // Actions (write-only atoms)
18 | export * from './actions'
19 |
20 | // Utilities
21 | export * from './utils'
22 |
--------------------------------------------------------------------------------
/packages/tailwind/src/state.js:
--------------------------------------------------------------------------------
1 | exports.base = {
2 | '--md-sys-state-hover-state-layer-opacity': '0.08',
3 | '--md-sys-state-focus-state-layer-opacity': '0.12',
4 | '--md-sys-state-pressed-state-layer-opacity': '0.12',
5 | '--md-sys-state-dragged-state-layer-opacity': '0.16',
6 | }
7 |
8 | exports.map = {
9 | hover: 'var(--md-sys-state-hover-state-layer-opacity)',
10 | focus: 'var(--md-sys-state-focus-state-layer-opacity)',
11 | pressed: 'var(--md-sys-state-pressed-state-layer-opacity)',
12 | dragged: 'var(--md-sys-state-dragged-state-layer-opacity)',
13 | }
14 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/value_objects/message_id.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class MessageId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | raise ValueError("MessageId value cannot be empty")
12 |
13 | try:
14 | uuid.UUID(self.value)
15 | except ValueError:
16 | raise ValueError("MessageId must be a valid UUID")
17 |
18 | @classmethod
19 | def generate(cls) -> "MessageId":
20 | return cls(str(uuid.uuid4()))
21 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/value_objects/podcast_id.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class PodcastId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | raise ValueError("PodcastId value cannot be empty")
12 |
13 | try:
14 | uuid.UUID(self.value)
15 | except ValueError:
16 | raise ValueError("PodcastId must be a valid UUID")
17 |
18 | @classmethod
19 | def generate(cls) -> "PodcastId":
20 | return cls(str(uuid.uuid4()))
21 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/schemas/annotation_schema.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from src.domain.annotation.value_objects.annotation_color import AnnotationColorEnum
4 | from src.domain.annotation.value_objects.annotation_type import AnnotationTypeEnum
5 | from src.presentation.api.schemas.base_schema import BaseSchemaModel
6 |
7 |
8 | class AnnotationSchema(BaseSchemaModel):
9 | id: str
10 | book_id: str
11 |
12 | cfi: str
13 | color: AnnotationColorEnum
14 | notes: str | None = None
15 | spine: dict[str, Any]
16 | text: str
17 | type: AnnotationTypeEnum
18 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/value_objects/annotation_text.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass(frozen=True)
5 | class AnnotationText:
6 | value: str
7 |
8 | def __post_init__(self) -> None:
9 | if not self.value:
10 | raise ValueError("Annotation text is required")
11 | if not isinstance(self.value, str):
12 | raise ValueError("Annotation text must be a string")
13 |
14 | @classmethod
15 | def from_string(cls, text_str: str) -> "AnnotationText":
16 | """文字列からAnnotationTextを生成"""
17 | return cls(text_str)
18 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/theme/useSourceColor.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import { useSettings } from '@flow/reader/utils/state'
4 |
5 | export function useSourceColor() {
6 | const [{ theme }, setSettings] = useSettings()
7 |
8 | const setSourceColor = useCallback(
9 | (source: string) => {
10 | setSettings((prev) => ({
11 | ...prev,
12 | theme: {
13 | ...prev.theme,
14 | source,
15 | },
16 | }))
17 | },
18 | [setSettings],
19 | )
20 |
21 | return { sourceColor: theme?.source ?? '#0ea5e9', setSourceColor }
22 | }
23 |
--------------------------------------------------------------------------------
/apps/reader/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.cjs",
8 | "css": "src/pages/styles.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@flow/reader/components",
15 | "utils": "@flow/reader/lib/utils",
16 | "ui": "@flow/reader/components/ui",
17 | "lib": "@flow/reader/lib",
18 | "hooks": "@flow/reader/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useMobile.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from 'jotai'
2 | import { useEffect } from 'react'
3 |
4 | export const mobileState = atom(undefined)
5 |
6 | let listened = false
7 |
8 | export function useMobile() {
9 | const [mobile, setMobile] = useAtom(mobileState)
10 |
11 | useEffect(() => {
12 | if (listened) return
13 | listened = true
14 |
15 | const mq = window.matchMedia('(max-width: 640px)')
16 | setMobile(mq.matches)
17 | mq.addEventListener('change', (e) => {
18 | setMobile(e.matches)
19 | })
20 | }, [setMobile])
21 |
22 | return mobile
23 | }
24 |
--------------------------------------------------------------------------------
/tsconfig.next.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "target": "es6",
7 | "lib": ["dom", "dom.iterable", "esnext"],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noEmit": true,
12 | "incremental": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve"
18 | },
19 | "include": ["src", "next-env.d.ts"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | from .podcast_exceptions import (
2 | PodcastAlreadyExistsError,
3 | PodcastAudioSynthesisError,
4 | PodcastException,
5 | PodcastGenerationError,
6 | PodcastInvalidStatusError,
7 | PodcastNotFoundError,
8 | PodcastScriptGenerationError,
9 | PodcastStorageError,
10 | )
11 |
12 | __all__ = [
13 | "PodcastException",
14 | "PodcastNotFoundError",
15 | "PodcastAlreadyExistsError",
16 | "PodcastGenerationError",
17 | "PodcastInvalidStatusError",
18 | "PodcastScriptGenerationError",
19 | "PodcastAudioSynthesisError",
20 | "PodcastStorageError",
21 | ]
22 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/loading/useLoadingState.ts:
--------------------------------------------------------------------------------
1 | import { useAtomValue } from 'jotai'
2 |
3 | import { isGlobalLoadingAtom, loadingTasksAtom } from '../../store/loading'
4 |
5 | /**
6 | * 基本的なローディング状態を管理するフック
7 | */
8 | export function useLoadingState(getCurrentTaskId: () => string | null) {
9 | const tasks = useAtomValue(loadingTasksAtom)
10 | const isGlobalLoading = useAtomValue(isGlobalLoadingAtom)
11 |
12 | // このフックインスタンスがローディング中かどうか
13 | const currentTaskId = getCurrentTaskId()
14 | const isLoading = currentTaskId ? tasks.has(currentTaskId) : false
15 |
16 | return {
17 | tasks,
18 | isGlobalLoading,
19 | isLoading,
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.message.entities import Message
2 | from src.domain.message.exceptions import (
3 | MessageAlreadyDeletedException,
4 | MessageDeliveryFailedException,
5 | MessageNotFoundException,
6 | )
7 | from src.domain.message.repositories import MessageRepository
8 | from src.domain.message.value_objects import (
9 | MessageContent,
10 | MessageId,
11 | )
12 |
13 | __all__ = [
14 | "Message",
15 | "MessageNotFoundException",
16 | "MessageAlreadyDeletedException",
17 | "MessageDeliveryFailedException",
18 | "MessageRepository",
19 | "MessageId",
20 | "MessageContent",
21 | ]
22 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | import { useEventListener } from './useEventListener'
4 |
5 | export function useMediaQuery(query: string) {
6 | const [mql, setMQL] = useState()
7 | const [matches, setMatches] = useState()
8 |
9 | useEffect(() => {
10 | setMQL(window.matchMedia(query))
11 | }, [query])
12 |
13 | useEffect(() => {
14 | if (mql) setMatches(mql.matches)
15 | }, [mql])
16 |
17 | useEventListener(mql as unknown as EventTarget, 'change', (e: Event) =>
18 | setMatches((e as MediaQueryListEvent).matches),
19 | )
20 |
21 | return matches
22 | }
23 |
--------------------------------------------------------------------------------
/apps/reader/src/components/Page.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import { ComponentProps } from 'react'
3 |
4 | interface PageProps extends ComponentProps<'div'> {
5 | headline: string
6 | }
7 | export const Page: React.FC = ({
8 | className,
9 | children,
10 | headline,
11 | ...props
12 | }) => {
13 | return (
14 |
15 |
22 | {headline}
23 |
24 | {children}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/apps/api/index_tenant_mapping.json:
--------------------------------------------------------------------------------
1 | {
2 | "BookContent_8eb3eeea": {
3 | "tenant_id": "test_user_id_alice",
4 | "file_name": "alice.epub",
5 | "created_at": "11b137d4-b230-4156-9af2-b4fab5a84ede"
6 | },
7 | "BookContent_729bd4be": {
8 | "tenant_id": "test_user_id_3a98e39b-173a-48ae-91a5-4d89b729e113",
9 | "file_name": "Fundamental-Accessibility-Tests-Basic-Functionality-v1.0.0.epub",
10 | "created_at": "49f41bfb-90d4-4870-a5f2-0b3b2ddb8623"
11 | },
12 | "BookContent_6d92e688": {
13 | "tenant_id": "test_user_id_58263899-d6bb-4ec6-905a-9e6e97269e03",
14 | "file_name": "alice.epub",
15 | "created_at": "f949d1e9-55e6-4b91-bfb9-15a3a6952ef4"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/epubjs/types/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for epubjs 0.3
2 | // Project: https://github.com/futurepress/epub.js#readme
3 | // Definitions by: Fred Chasen
4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5 | import Epub from './epub'
6 |
7 | export as namespace ePub
8 |
9 | export default Epub
10 |
11 | export { default as Book } from './book'
12 | export { default as EpubCFI } from './epubcfi'
13 | export { Rendition, Location } from './rendition'
14 | export { default as Contents } from './contents'
15 | export { default as Layout } from './layout'
16 | export { NavItem } from './navigation'
17 |
18 | declare namespace ePub {}
19 |
--------------------------------------------------------------------------------
/packages/epubjs/types/spine.d.ts:
--------------------------------------------------------------------------------
1 | import Packaging from './packaging'
2 | import Section from './section'
3 | import Hook from './utils/hook'
4 |
5 | export default class Spine {
6 | constructor()
7 |
8 | hooks: {
9 | serialize: Hook
10 | content: Hook
11 | }
12 |
13 | unpack(_package: Packaging, resolver: Function, canonical: Function): void
14 |
15 | get(target?: string | number): Section
16 |
17 | each(...args: any[]): any
18 |
19 | first(): Section
20 |
21 | last(): Section
22 |
23 | destroy(): void
24 |
25 | private append(section: Section): number
26 |
27 | private prepend(section: Section): number
28 |
29 | private remove(section: Section): number
30 | }
31 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/exceptions/chat_exceptions.py:
--------------------------------------------------------------------------------
1 | class ChatError(Exception):
2 | pass
3 |
4 |
5 | class ChatNotFoundError(ChatError):
6 | def __init__(self, message: str = "Chat not found") -> None:
7 | self.message = message
8 | super().__init__(self.message)
9 |
10 |
11 | class ChatAlreadyExistsError(ChatError):
12 | def __init__(self, message: str = "Chat already exists") -> None:
13 | self.message = message
14 | super().__init__(self.message)
15 |
16 |
17 | class ChatValidationError(ChatError):
18 | def __init__(self, message: str = "Chat validation failed") -> None:
19 | self.message = message
20 | super().__init__(self.message)
21 |
--------------------------------------------------------------------------------
/packages/epubjs/types/pagelist.d.ts:
--------------------------------------------------------------------------------
1 | export interface PageListItem {
2 | href: string
3 | page: string
4 | cfi?: string
5 | packageUrl?: string
6 | }
7 |
8 | export default class Pagelist {
9 | constructor(xml: XMLDocument)
10 |
11 | parse(xml: XMLDocument): Array
12 |
13 | pageFromCfi(cfi: string): number
14 |
15 | cfiFromPage(pg: string | number): string
16 |
17 | pageFromPercentage(percent: number): number
18 |
19 | percentageFromPage(pg: number): number
20 |
21 | destroy(): void
22 |
23 | private parseNav(navHtml: Node): Array
24 |
25 | private item(item: Node): PageListItem
26 |
27 | private process(pageList: Array): void
28 | }
29 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './theme'
2 | export * from './useAction'
3 | export * from './useAsync'
4 | export * from './useDisablePinchZooming'
5 | export * from './useEnv'
6 | export * from './useForceRender'
7 | export * from './useSWR/useLibrary'
8 | export * from './useList'
9 | export * from './useMobile'
10 | export * from './useTextSelection'
11 | export * from './useTranslation'
12 | export * from './useTypography'
13 | export * from './useAfterMount'
14 | export * from './useBoolean'
15 | export * from './useEventListener'
16 | export * from './useHover'
17 | export * from './useMediaQuery'
18 | export * from './usePress'
19 | export * from './podcast/usePodcastActions'
20 | export * from './podcast/useAudioControls'
21 |
--------------------------------------------------------------------------------
/packages/epubjs/types/utils/queue.d.ts:
--------------------------------------------------------------------------------
1 | import { defer } from './core'
2 |
3 | export interface QueuedTask {
4 | task: any | Task
5 | args: any[]
6 | deferred: any // should be defer, but not working
7 | promise: Promise
8 | }
9 |
10 | export default class Queue {
11 | constructor(context: any)
12 |
13 | enqueue(func: Promise | Function, ...args: any[]): Promise
14 |
15 | dequeue(): Promise
16 |
17 | dump(): void
18 |
19 | run(): Promise
20 |
21 | flush(): Promise
22 |
23 | clear(): void
24 |
25 | length(): number
26 |
27 | pause(): void
28 |
29 | stop(): void
30 | }
31 |
32 | declare class Task {
33 | constructor(task: any, args: any[], context: any)
34 | }
35 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/value_objects/book_id.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class BookId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | raise ValueError("BookIdは必須です")
12 |
13 | if not self._is_valid_uuid(self.value):
14 | raise ValueError("BookIdは有効なUUID形式である必要があります")
15 |
16 | @staticmethod
17 | def _is_valid_uuid(val: str) -> bool:
18 | try:
19 | uuid.UUID(str(val))
20 | return True
21 | except ValueError:
22 | return False
23 |
24 | @classmethod
25 | def generate(cls) -> "BookId":
26 | return cls(str(uuid.uuid4()))
27 |
--------------------------------------------------------------------------------
/apps/reader/src/components/FormattedText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | function cleanUpText(str: string, strict = false): string {
4 | str = str.replace(/^\s+|\s+$/g, '')
5 |
6 | if (strict) {
7 | str = str.replace(/\n{2,}/g, '\n')
8 | }
9 |
10 | return str
11 | }
12 |
13 | export const FormattedText = ({
14 | text,
15 | strict = false,
16 | className,
17 | }: {
18 | text: string
19 | strict?: boolean
20 | className?: string
21 | }) => (
22 |
23 | {cleanUpText(text, strict)
24 | .split('\n')
25 | .map((line, index) => (
26 |
27 | {line}
28 |
29 |
30 | ))}
31 |
32 | )
33 |
--------------------------------------------------------------------------------
/apps/api/src/config/seed.py:
--------------------------------------------------------------------------------
1 | from src.config.app_config import TEST_USER_ID
2 | from src.config.db import SessionLocal
3 | from src.infrastructure.postgres.user.user_dto import UserDTO
4 |
5 | # ※ 初回のみ、テーブル作成を実行(すでにテーブルが存在する場合は不要)
6 | # Base.metadata.create_all(bind=engine)
7 |
8 |
9 | def seed_data():
10 | session = SessionLocal()
11 | try:
12 | # シードデータの作成例
13 | seed_items = [UserDTO(id=TEST_USER_ID, username="testuser", email="example@example.com")]
14 |
15 | # 複数のシードデータを一括で追加
16 | session.add_all(seed_items)
17 | session.commit()
18 | except Exception:
19 | session.rollback()
20 | finally:
21 | session.close()
22 |
23 |
24 | if __name__ == "__main__":
25 | seed_data()
26 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useDisablePinchZooming.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | // https://github.com/excalidraw/excalidraw/blob/7eaf47c9d41a33a6230d8c3a16b5087fc720dcfb/src/packages/excalidraw/index.tsx#L66
4 | export function useDisablePinchZooming(win?: Window) {
5 | useEffect(() => {
6 | const _win = win ?? window
7 | // Block pinch-zooming on iOS outside of the content area
8 | const handleTouchMove = (event: TouchEvent) => {
9 | event.preventDefault()
10 | }
11 |
12 | _win.document.addEventListener('touchmove', handleTouchMove, {
13 | passive: false,
14 | })
15 |
16 | return () => {
17 | _win.document.removeEventListener('touchmove', handleTouchMove)
18 | }
19 | }, [win])
20 | }
21 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/vector.py:
--------------------------------------------------------------------------------
1 | from langchain_weaviate.vectorstores import WeaviateVectorStore
2 |
3 | from src.infrastructure.memory.memory_vector_store import MemoryVectorStore
4 |
5 |
6 | def get_book_content_vector_store() -> WeaviateVectorStore:
7 | """BookContent 用の VectorStore を取得する.
8 |
9 | MemoryVectorStore で生成済みの共有クライアント/Embedding を再利用し、
10 | 不要なコネクションやモデルの重複ロードを防ぐ。
11 | """
12 | try:
13 | return WeaviateVectorStore(
14 | client=MemoryVectorStore.get_client(),
15 | text_key="content",
16 | index_name=MemoryVectorStore.BOOK_CONTENT_COLLECTION_NAME,
17 | embedding=MemoryVectorStore.get_embedding_model(),
18 | )
19 | except Exception:
20 | raise
21 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/handlers/annotation_api_route_handler.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends, status
2 |
3 | from src.infrastructure.di.injection import get_sync_annotations_usecase
4 | from src.presentation.api.schemas.book_schema import BookUpdateRequest
5 | from src.usecase.annotation.update_annotation_use_case import SyncAnnotationsUseCase
6 |
7 | router = APIRouter()
8 |
9 |
10 | @router.put("", status_code=status.HTTP_204_NO_CONTENT)
11 | async def update_annotation(
12 | book_id: str,
13 | changes: BookUpdateRequest,
14 | sync_annotations_usecase: SyncAnnotationsUseCase = Depends(get_sync_annotations_usecase),
15 | ) -> None:
16 | sync_annotations_usecase.execute(book_id=book_id, annotations=changes.annotations)
17 |
--------------------------------------------------------------------------------
/apps/reader/src/types/loading.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Loading task types and interfaces
3 | */
4 |
5 | export interface LoadingTask {
6 | id: string
7 | message?: string
8 | startTime?: number
9 | progress?: {
10 | current: number
11 | total: number
12 | }
13 | type: 'global' | 'local'
14 | canCancel?: boolean
15 | icon?: string
16 | subTasks?: {
17 | currentFileName?: string
18 | filesCompleted: number
19 | filesTotal: number
20 | }
21 | }
22 |
23 | export type LoadingTaskUpdate = Partial
24 |
25 | export interface LoadingTaskWithUpdates {
26 | id: string
27 | updates: LoadingTaskUpdate
28 | }
29 |
30 | export type LoadingTaskType = 'global' | 'local'
31 |
32 | export type LoadingTaskMap = Map
33 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '../../lib/utils'
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<'textarea'>
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = 'Textarea'
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/message/__init__.py:
--------------------------------------------------------------------------------
1 | from src.usecase.message.create_message_usecase import CreateMessageUseCase, CreateMessageUseCaseImpl
2 | from src.usecase.message.delete_message_usecase import DeleteMessageUseCase, DeleteMessageUseCaseImpl
3 | from src.usecase.message.find_message_by_id_usecase import (
4 | FindMessageByIdUseCase,
5 | FindMessageByIdUseCaseImpl,
6 | )
7 | from src.usecase.message.find_messages_usecase import FindMessagesUseCase, FindMessagesUseCaseImpl
8 |
9 | __all__ = [
10 | "CreateMessageUseCase",
11 | "CreateMessageUseCaseImpl",
12 | "DeleteMessageUseCase",
13 | "DeleteMessageUseCaseImpl",
14 | "FindMessageByIdUseCase",
15 | "FindMessageByIdUseCaseImpl",
16 | "FindMessagesUseCase",
17 | "FindMessagesUseCaseImpl",
18 | ]
19 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/epub.ts:
--------------------------------------------------------------------------------
1 | import ePub from '@flow/epubjs'
2 |
3 | import { fileToBase64 } from './fileUtils'
4 |
5 | export async function fileToEpub(file: File) {
6 | const data = await file.arrayBuffer()
7 | return ePub(data)
8 | }
9 |
10 | export const indexEpub = async (file: File, userId: string, bookId: string) => {
11 | try {
12 | await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/rag`, {
13 | method: 'POST',
14 | headers: {
15 | 'Content-Type': 'application/json',
16 | },
17 | body: JSON.stringify({
18 | userId,
19 | bookId,
20 | fileData: await fileToBase64(file),
21 | fileName: file.name,
22 | }),
23 | })
24 | } catch (error) {
25 | console.error('アップロード中のエラー:', error)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/book/create_book_vector_index_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from fastapi import UploadFile
4 |
5 | from src.infrastructure.memory.memory_vector_store import MemoryVectorStore
6 |
7 |
8 | class CreateBookVectorIndexUseCase(ABC):
9 | @abstractmethod
10 | async def execute(self, file: UploadFile, user_id: str, book_id: str) -> dict:
11 | """EPUBファイルを処理してベクトルストアにインデックス化する."""
12 |
13 |
14 | class CreateBookVectorIndexUseCaseImpl(CreateBookVectorIndexUseCase):
15 | def __init__(self) -> None:
16 | self.memory_vector_store = MemoryVectorStore()
17 |
18 | async def execute(self, file: UploadFile, user_id: str, book_id: str) -> dict:
19 | return await self.memory_vector_store.create_book_vector_index(file, user_id, book_id)
20 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/theme/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import useLocalStorageState from 'use-local-storage-state'
3 |
4 | import { useMediaQuery } from '../../hooks'
5 |
6 | export type ColorScheme = 'light' | 'dark' | 'system'
7 |
8 | export function useColorScheme() {
9 | const [scheme, setScheme] = useLocalStorageState(
10 | 'literal-color-scheme',
11 | { defaultValue: 'system' },
12 | )
13 |
14 | const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
15 | const dark = scheme === 'dark' || (scheme === 'system' && prefersDark)
16 |
17 | useEffect(() => {
18 | if (dark !== undefined) {
19 | document.documentElement.classList.toggle('dark', dark)
20 | }
21 | }, [dark])
22 |
23 | return { scheme, dark, setScheme }
24 | }
25 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/value_objects/annotation_id.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class AnnotationId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | object.__setattr__(self, "value", str(uuid.uuid4()))
12 | elif not isinstance(self.value, str):
13 | raise ValueError("AnnotationId must be a string")
14 |
15 | @classmethod
16 | def new(cls) -> "AnnotationId":
17 | """新しいIDを生成"""
18 | return cls(str(uuid.uuid4()))
19 |
20 | @classmethod
21 | def from_string(cls, id_str: str | None) -> "AnnotationId":
22 | """文字列からAnnotationIdを生成"""
23 | if not id_str:
24 | return cls.new()
25 | return cls(id_str)
26 |
--------------------------------------------------------------------------------
/packages/tailwind/src/elevation.js:
--------------------------------------------------------------------------------
1 | const KEY = 'rgba(0, 0, 0, 0.15)'
2 | const AMBIENT = 'rgba(0, 0, 0, 0.3)'
3 |
4 | exports.base = {
5 | '--md-sys-elevation-level1': `0px 1px 3px 1px ${KEY}, 0px 1px 2px ${AMBIENT};`,
6 | '--md-sys-elevation-level2': `0px 2px 6px 2px ${KEY}, 0px 1px 2px ${AMBIENT};`,
7 | '--md-sys-elevation-level3': `0px 4px 8px 3px ${KEY}, 0px 1px 3px ${AMBIENT};`,
8 | '--md-sys-elevation-level4': `0px 6px 10px 4px ${KEY}, 0px 2px 3px ${AMBIENT};`,
9 | '--md-sys-elevation-level5': `0px 8px 12px 6px ${KEY}, 0px 4px 4px ${AMBIENT};`,
10 | }
11 |
12 | exports.map = {
13 | 1: 'var(--md-sys-elevation-level1)',
14 | 2: 'var(--md-sys-elevation-level2)',
15 | 3: 'var(--md-sys-elevation-level3)',
16 | 4: 'var(--md-sys-elevation-level4)',
17 | 5: 'var(--md-sys-elevation-level5)',
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "inlineSources": false,
9 | "isolatedModules": true,
10 | "moduleResolution": "node",
11 | "noUnusedLocals": false,
12 | "noUnusedParameters": false,
13 | "preserveWatchOutput": true,
14 | "skipLibCheck": true,
15 | "noUncheckedIndexedAccess": true,
16 | "strict": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@flow/reader/*": ["apps/reader/src/*"],
20 | "@flow/internal": ["packages/internal/src/index.ts"],
21 | "@flow/epubjs/*": ["packages/epubjs/*"]
22 | }
23 | },
24 | "exclude": ["node_modules"]
25 | }
26 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | from src.domain.book.exceptions.book_exceptions import (
2 | BookAlreadyCompletedException as BookAlreadyCompletedException,
3 | )
4 | from src.domain.book.exceptions.book_exceptions import (
5 | BookAlreadyStartedException as BookAlreadyStartedException,
6 | )
7 | from src.domain.book.exceptions.book_exceptions import (
8 | BookDomainException as BookDomainException,
9 | )
10 | from src.domain.book.exceptions.book_exceptions import (
11 | BookFileNotFoundException as BookFileNotFoundException,
12 | )
13 | from src.domain.book.exceptions.book_exceptions import (
14 | BookNotFoundException as BookNotFoundException,
15 | )
16 | from src.domain.book.exceptions.book_exceptions import (
17 | BookPermissionDeniedException as BookPermissionDeniedException,
18 | )
19 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/chat/find_chats_by_user_id_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.chat.entities.chat import Chat
4 | from src.domain.chat.repositories.chat_repository import ChatRepository
5 | from src.domain.chat.value_objects.user_id import UserId
6 |
7 |
8 | class FindChatsByUserIdUseCase(ABC):
9 | @abstractmethod
10 | def execute(self, user_id: UserId) -> list[Chat]:
11 | """ユーザーIDに紐づくChatをすべて取得する"""
12 |
13 |
14 | class FindChatsByUserIdUseCaseImpl(FindChatsByUserIdUseCase):
15 | def __init__(self, chat_repository: ChatRepository) -> None:
16 | self.chat_repository = chat_repository
17 |
18 | def execute(self, user_id: UserId) -> list[Chat]:
19 | """ユーザーIDに紐づくChatをすべて取得する"""
20 | return self.chat_repository.find_by_user_id(user_id)
21 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/value_objects/tennant_id.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class TenantId:
7 | value: str
8 |
9 | def __post_init__(self) -> None:
10 | if not self.value:
11 | raise ValueError("Tenant ID is required")
12 |
13 | @staticmethod
14 | def generate(user_id: str, book_id: str) -> "TenantId":
15 | user_hex = uuid.UUID(user_id).hex
16 | book_hex = uuid.UUID(book_id).hex
17 | encoded = user_hex + book_hex
18 | return TenantId(encoded)
19 |
20 | def get_user_id(self) -> str:
21 | decoded = self.value[:32]
22 | return str(uuid.UUID(decoded))
23 |
24 | def get_book_id(self) -> str:
25 | decoded = self.value[32:]
26 | return str(uuid.UUID(decoded))
27 |
--------------------------------------------------------------------------------
/apps/reader/src/components/reader/ReaderGridView.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import React from 'react'
3 |
4 | import { useEventListener } from '../../hooks'
5 | import { reader, useReaderSnapshot } from '../../models'
6 | import { SplitView } from '../base/SplitView'
7 |
8 | import { ReaderGroup } from './ReaderGroup'
9 | import { handleKeyDown } from './eventHandlers'
10 |
11 | export function ReaderGridView() {
12 | const { groups } = useReaderSnapshot()
13 |
14 | useEventListener('keydown', handleKeyDown(reader.focusedBookTab))
15 |
16 | if (!groups.length) return null
17 | return (
18 |
19 | {/* @ts-ignore */}
20 | {groups.map((group, i) => (
21 |
22 | ))}
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useTypography.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useSnapshot } from 'valtio'
3 |
4 | import { BookTab } from '../models'
5 | import { useSettings } from '../utils/state'
6 |
7 | function removeUndefinedProperty>(obj: T) {
8 | const newObj: Partial = {}
9 |
10 | Object.entries(obj).forEach(([k, v]) => {
11 | if (v !== undefined) {
12 | newObj[k as keyof T] = v
13 | }
14 | })
15 |
16 | return newObj
17 | }
18 |
19 | export function useTypography(tab: BookTab) {
20 | const { book } = useSnapshot(tab)
21 | const [settings] = useSettings()
22 |
23 | return useMemo(
24 | () => ({
25 | ...settings,
26 | ...removeUndefinedProperty(book.configuration?.typography ?? {}),
27 | }),
28 | [book.configuration?.typography, settings],
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/apps/api/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 | services:
3 | weaviate:
4 | image: cr.weaviate.io/semitechnologies/weaviate:latest
5 | ports:
6 | - '8080:8080'
7 | - '50051:50051'
8 | restart: on-failure:0
9 | environment:
10 | QUERY_DEFAULTS_LIMIT: 20
11 | AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
12 | PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
13 | DEFAULT_VECTORIZER_MODULE: 'none'
14 | CLUSTER_HOSTNAME: 'node1'
15 | volumes:
16 | - weaviate_data:/var/lib/weaviate
17 |
18 | gcloud-storage-emulator:
19 | image: fsouza/fake-gcs-server:latest
20 | ports:
21 | - 4443:4443
22 | command: -scheme http -public-host ${URL:-localhost}:4443
23 | volumes:
24 | - ./gcs_fixtures:/data
25 | - ./gcs:/storage
26 |
27 | volumes:
28 | weaviate_data:
29 | gcs_fixtures:
30 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/repositories/book_repository.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.book.entities.book import Book
4 | from src.domain.book.value_objects.book_id import BookId
5 |
6 |
7 | class BookRepository(ABC):
8 | @abstractmethod
9 | def save(self, book: Book) -> None:
10 | pass
11 |
12 | @abstractmethod
13 | def find_by_id(self, book_id: BookId) -> Book | None:
14 | pass
15 |
16 | @abstractmethod
17 | def find_all(self) -> list[Book]:
18 | pass
19 |
20 | @abstractmethod
21 | def find_by_user_id(self, user_id: str) -> list[Book]:
22 | pass
23 |
24 | @abstractmethod
25 | def delete(self, book_id: BookId) -> None:
26 | pass
27 |
28 | @abstractmethod
29 | def bulk_delete(self, book_ids: list[BookId]) -> list[BookId]:
30 | pass
31 |
--------------------------------------------------------------------------------
/packages/epubjs/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "epubjs",
3 | "version": "0.3.0",
4 | "authors": ["Fred Chasen "],
5 | "description": "Enhanced eBooks in the browser.",
6 | "main": "dist/epub.js",
7 | "moduleType": ["amd", "globals", "node"],
8 | "keywords": ["epub"],
9 | "license": "MIT",
10 | "homepage": "http://futurepress.org",
11 | "ignore": [
12 | "**/.*",
13 | "node_modules",
14 | "bower_components",
15 | "test",
16 | "tools",
17 | "books",
18 | "examples"
19 | ],
20 | "dependencies": {
21 | "event-emitter": "^0.3.5",
22 | "jszip": "^3.4.0",
23 | "localforage": "^1.7.3",
24 | "lodash": "^4.17.15",
25 | "marks-pane": "^1.0.9",
26 | "path-webpack": "0.0.3",
27 | "stream-browserify": "^3.0.0",
28 | "url-polyfill": "^1.1.9",
29 | "xmldom": "^0.3.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ErrorInfo, ReactNode } from 'react'
2 |
3 | interface Props {
4 | children?: ReactNode
5 | }
6 |
7 | interface State {
8 | hasError: boolean
9 | }
10 |
11 | export class ErrorBoundary extends Component {
12 | public state: State = {
13 | hasError: false,
14 | }
15 |
16 | public static getDerivedStateFromError(_: Error): State {
17 | // Update state so the next render will show the fallback UI.
18 | return { hasError: true }
19 | }
20 |
21 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
22 | console.error('Uncaught error:', error, errorInfo)
23 | }
24 |
25 | public render() {
26 | if (this.state.hasError) {
27 | return Sorry.. there was an error
28 | }
29 |
30 | return this.props.children
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/apps/reader/src/components/reader/eventHandlers.ts:
--------------------------------------------------------------------------------
1 | import { BookTab } from '../../models'
2 |
3 | export function handleKeyDown(tab?: BookTab) {
4 | return (e: KeyboardEvent) => {
5 | const activeElement = document.activeElement
6 |
7 | if (
8 | !activeElement ||
9 | activeElement.matches('input, textarea, select, [contenteditable]')
10 | ) {
11 | return
12 | }
13 |
14 | try {
15 | switch (e.code) {
16 | case 'ArrowLeft':
17 | case 'ArrowUp':
18 | tab?.prev()
19 | break
20 | case 'ArrowRight':
21 | case 'ArrowDown':
22 | tab?.next()
23 | break
24 | case 'Space':
25 | e.shiftKey ? tab?.prev() : tab?.next()
26 | break
27 | }
28 | } catch {
29 | // ignore `rendition is undefined` error
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/apps/reader/src/pages/success.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | export default function Success() {
4 | const [countdown, setCountdown] = useState(3)
5 |
6 | useEffect(() => {
7 | const id = setInterval(() => {
8 | setCountdown((cd) => {
9 | if (cd > 1) return cd - 1
10 |
11 | clearInterval(id)
12 | window.close()
13 | return cd
14 | })
15 | }, 1000)
16 | }, [])
17 |
18 | return (
19 |
20 |
21 |
22 | Oauth success
23 |
24 |
25 | This window will close in {countdown}s.
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/exceptions/message_exceptions.py:
--------------------------------------------------------------------------------
1 | class MessageDomainException(Exception): # noqa: N818
2 | """Messageドメインに関連する例外の基底クラス"""
3 |
4 |
5 | class MessageNotFoundException(MessageDomainException):
6 | def __init__(self, message_id: str) -> None:
7 | self.message_id = message_id
8 | super().__init__(f"ID {message_id} のメッセージが見つかりません")
9 |
10 |
11 | class MessageAlreadyDeletedException(MessageDomainException):
12 | def __init__(self) -> None:
13 | super().__init__("このメッセージは既に削除されています")
14 |
15 |
16 | class MessageDeliveryFailedException(MessageDomainException):
17 | def __init__(self) -> None:
18 | super().__init__("メッセージの配信に失敗しました")
19 |
20 |
21 | class MessagePermissionDeniedException(MessageDomainException):
22 | def __init__(self) -> None:
23 | super().__init__("このメッセージへのアクセス権限がありません")
24 |
--------------------------------------------------------------------------------
/apps/reader/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooksPlugin from 'eslint-plugin-react-hooks'
2 | import tseslint from 'typescript-eslint'
3 |
4 | import baseConfig from '../../eslint.config.js'
5 |
6 | export default tseslint.config(...baseConfig, {
7 | files: ['**/*.ts', '**/*.tsx'],
8 | plugins: {
9 | 'react-hooks': reactHooksPlugin,
10 | },
11 | rules: {
12 | '@next/next/no-html-link-for-pages': 'off',
13 | '@next/next/no-img-element': 'off',
14 | 'react/jsx-key': 'off',
15 | 'react/no-children-prop': 'off',
16 | '@typescript-eslint/no-unused-expressions': [
17 | 'error',
18 | {
19 | allowTernary: true,
20 | },
21 | ],
22 | 'react-hooks/exhaustive-deps': [
23 | 'warn',
24 | {
25 | additionalHooks: 'useRecoilCallback|useRecoilTransaction_UNSTABLE',
26 | },
27 | ],
28 | },
29 | })
30 |
--------------------------------------------------------------------------------
/apps/reader/sentry.client.config.js:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the browser.
2 | // The config you add here will be used whenever a page is visited.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from '@sentry/nextjs'
6 |
7 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN
8 |
9 | Sentry.init({
10 | dsn:
11 | SENTRY_DSN ||
12 | 'https://911830b959464866b3820e27379f4d38@o955619.ingest.sentry.io/6537954',
13 | // Adjust this value in production, or use tracesSampler for greater control
14 | tracesSampleRate: 1.0,
15 | // ...
16 | // Note: if you want to override the automatic release value, do not set a
17 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so
18 | // that it will also get attached to your source maps
19 | })
20 |
--------------------------------------------------------------------------------
/packages/epubjs/examples/contents.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | EPUB.js Basic Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.claude/settings.local.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissions": {
3 | "allow": [
4 | "Bash(find:*)",
5 | "Bash(ls:*)",
6 | "Bash(mkdir:*)",
7 | "Bash(pip install:*)",
8 | "Bash(python:*)",
9 | "Bash(yamllint:*)",
10 | "Bash(pnpm build)",
11 | "Bash(pnpm ts:check:*)",
12 | "Bash(cat:*)",
13 | "Bash(mv:*)",
14 | "Bash(pnpm lint:*)",
15 | "Bash(pnpm dev:*)",
16 | "Bash(pnpm add:*)",
17 | "WebFetch(domain:python.langchain.com)",
18 | "Bash(make lint)",
19 | "WebFetch(domain:github.com)",
20 | "Bash(poetry run:*)",
21 | "Bash(poetry show:*)",
22 | "Bash(poetry:*)",
23 | "Bash(grep:*)",
24 | "Bash(pnpm openapi:ts:*)",
25 | "Bash(git:*)"
26 | ]
27 | },
28 | "enableAllProjectMcpServers": true,
29 | "enabledMcpjsonServers": ["playwright"],
30 | "deny": []
31 | }
32 |
--------------------------------------------------------------------------------
/apps/reader/sentry.server.config.js:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the server.
2 | // The config you add here will be used whenever the server handles a request.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from '@sentry/nextjs'
6 |
7 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN
8 |
9 | Sentry.init({
10 | dsn:
11 | SENTRY_DSN ||
12 | 'https://911830b959464866b3820e27379f4d38@o955619.ingest.sentry.io/6537954',
13 | // Adjust this value in production, or use tracesSampler for greater control
14 | tracesSampleRate: 1.0,
15 | // ...
16 | // Note: if you want to override the automatic release value, do not set a
17 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so
18 | // that it will also get attached to your source maps
19 | })
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 | dist
16 |
17 | # misc
18 | .DS_Store
19 | *.pem
20 |
21 | # debug
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | .pnpm-debug.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # turbo
35 | .turbo
36 |
37 | # PWA
38 | **/public/workbox-*
39 | **/public/sw.*
40 | **/public/fallback-*
41 |
42 | # worktree
43 | tmp-worktree/
44 |
45 | # cursor
46 | .cursor
47 |
48 | # roo
49 | .roo
50 |
51 | # repomix
52 | repomix-output.xml
53 |
54 | # debug
55 | launch.json
56 |
57 | # github
58 | .github/prompts
59 |
60 | # local
61 | *.local*
62 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/state.ts:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from 'jotai'
2 | import { atomWithStorage } from 'jotai/utils'
3 |
4 | import { RenditionSpread } from '@flow/epubjs/types/rendition'
5 |
6 | export const navbarState = atom(false)
7 |
8 | export interface Settings extends TypographyConfiguration {
9 | theme?: ThemeConfiguration
10 | }
11 |
12 | export interface TypographyConfiguration {
13 | fontSize?: string
14 | fontWeight?: number
15 | fontFamily?: string
16 | lineHeight?: number
17 | spread?: RenditionSpread
18 | zoom?: number
19 | }
20 |
21 | interface ThemeConfiguration {
22 | source?: string
23 | background?: number
24 | }
25 |
26 | export const defaultSettings: Settings = {}
27 |
28 | const settingsState = atomWithStorage('settings', defaultSettings)
29 |
30 | export function useSettings() {
31 | return useAtom(settingsState)
32 | }
33 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@flow/reader/lib/utils'
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | },
19 | )
20 | Input.displayName = 'Input'
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useIntermediateKeyword.ts:
--------------------------------------------------------------------------------
1 | // When inputting with IME and storing state in `valtio`,
2 | // unexpected rendering with `e.target.value === ''` occurs,
3 | // which leads to ` ` and IME flash to empty,
4 | // while this will not happen when using `React.useState`,
5 |
6 | import { useState, useEffect } from 'react'
7 |
8 | import { useReaderSnapshot, reader } from '../models'
9 |
10 | // so we should create an intermediate `keyword` state to fix this.
11 | export function useIntermediateKeyword() {
12 | const [keyword, setKeyword] = useState('')
13 | const { focusedBookTab } = useReaderSnapshot()
14 |
15 | useEffect(() => {
16 | setKeyword(focusedBookTab?.keyword ?? '')
17 | }, [focusedBookTab?.keyword])
18 |
19 | useEffect(() => {
20 | reader.focusedBookTab?.setKeyword(keyword)
21 | }, [keyword])
22 |
23 | return [keyword, setKeyword] as const
24 | }
25 |
--------------------------------------------------------------------------------
/apps/api/src/config/app_config.py:
--------------------------------------------------------------------------------
1 | from typing import Self
2 |
3 | from pydantic import Field
4 | from pydantic_settings import BaseSettings
5 |
6 | TEST_USER_ID = "91527c9d-48aa-41d0-bb85-dc96f26556a0"
7 | DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small"
8 |
9 |
10 | class AppConfig(BaseSettings):
11 | gcs_emulator_host: str | None = Field(default=None, description="Cloud Storage Emulator Host")
12 | database_url: str = Field(description="データベースURL")
13 | gcp_project_id: str = Field(default="bookwith", description="Google Cloud Project ID")
14 | gcs_bucket_name: str = Field(default="bookwith-bucket", description="GCS bucket name")
15 | gemini_api_key: str | None = Field(default=None, description="Gemini API Key")
16 | openai_api_key: str = Field(min_length=1, description="OpenAI API Key")
17 |
18 | @classmethod
19 | def get_config(cls) -> Self:
20 | return cls()
21 |
--------------------------------------------------------------------------------
/packages/tailwind/src/index.js:
--------------------------------------------------------------------------------
1 | const plugin = require('tailwindcss/plugin')
2 |
3 | const colors = require('./colors')
4 | const elevation = require('./elevation')
5 | const state = require('./state')
6 | const typography = require('./typography')
7 |
8 | module.exports = plugin.withOptions(
9 | () => {
10 | return ({ addUtilities, addBase }) => {
11 | addBase({
12 | ':root': {
13 | ...typography.base,
14 | ...state.base,
15 | ...elevation.base,
16 | },
17 | })
18 |
19 | addUtilities({
20 | ...colors.utilities,
21 | ...typography.utilities,
22 | })
23 | }
24 | },
25 | function () {
26 | return {
27 | theme: {
28 | extend: {
29 | colors: colors.theme,
30 | opacity: state.map,
31 | boxShadow: elevation.map,
32 | },
33 | },
34 | }
35 | },
36 | )
37 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useSWR/useLibrary.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 |
3 | import { components } from '../../lib/openapi-schema/schema'
4 | import { TEST_USER_ID } from '../../pages/_app'
5 |
6 | import { fetcher } from './fetcher'
7 |
8 | export function useLibrary() {
9 | const { data, error, mutate } = useSWR<
10 | components['schemas']['BooksResponse']
11 | >(
12 | `${process.env.NEXT_PUBLIC_API_BASE_URL}/books/user/${TEST_USER_ID}`,
13 | fetcher,
14 | )
15 |
16 | return {
17 | books: data?.books || [],
18 | error,
19 | mutate,
20 | }
21 | }
22 |
23 | export function useBookCovers() {
24 | const { data, mutate, isValidating } = useSWR<
25 | components['schemas']['CoversResponse']
26 | >(`${process.env.NEXT_PUBLIC_API_BASE_URL}/books/covers`, fetcher)
27 |
28 | return {
29 | covers: data?.covers || [],
30 | mutate,
31 | isCoverLoading: isValidating,
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/color.ts:
--------------------------------------------------------------------------------
1 | import {
2 | redFromArgb,
3 | greenFromArgb,
4 | blueFromArgb,
5 | argbFromRgb,
6 | hexFromArgb,
7 | } from '@material/material-color-utilities'
8 |
9 | export function rgbFromArgb(argb: number) {
10 | return [redFromArgb, greenFromArgb, blueFromArgb].map((f) => f(argb))
11 | }
12 |
13 | function compositeChannels(channel1: number, channel2: number, p: number) {
14 | return (1 - p) * channel1 + p * channel2
15 | }
16 |
17 | // https://en.wikipedia.org/wiki/Transparency_%28graphic%29#Compositing_calculations
18 | export function compositeColors(color1: number, color2: number, p: number) {
19 | const [r1, g1, b1] = rgbFromArgb(color1)
20 | const [r2, g2, b2] = rgbFromArgb(color2)
21 | return hexFromArgb(
22 | argbFromRgb(
23 | compositeChannels(r1!, r2!, p),
24 | compositeChannels(g1!, g2!, p),
25 | compositeChannels(b1!, b2!, p),
26 | ),
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/packages/epubjs/types/archive.d.ts:
--------------------------------------------------------------------------------
1 | import JSZip = require('jszip')
2 |
3 | export default class Archive {
4 | constructor()
5 |
6 | open(input: BinaryType, isBase64?: boolean): Promise
7 |
8 | openUrl(zipUrl: string, isBase64?: boolean): Promise
9 |
10 | request(
11 | url: string,
12 | type?: string,
13 | ): Promise
14 |
15 | getBlob(url: string, mimeType?: string): Promise
16 |
17 | getText(url: string): Promise
18 |
19 | getBase64(url: string, mimeType?: string): Promise
20 |
21 | createUrl(url: string, options: { base64: boolean }): Promise
22 |
23 | revokeUrl(url: string): void
24 |
25 | destroy(): void
26 |
27 | private checkRequirements(): void
28 |
29 | private handleResponse(
30 | response: any,
31 | type?: string,
32 | ): Blob | string | JSON | Document | XMLDocument
33 | }
34 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as ProgressPrimitive from '@radix-ui/react-progress'
2 | import * as React from 'react'
3 |
4 | import { cn } from '@flow/reader/lib/utils'
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/chat/delete_chat_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.chat.exceptions.chat_exceptions import ChatNotFoundError
4 | from src.domain.chat.repositories.chat_repository import ChatRepository
5 | from src.domain.chat.value_objects.chat_id import ChatId
6 |
7 |
8 | class DeleteChatUseCase(ABC):
9 | @abstractmethod
10 | def execute(self, chat_id: ChatId) -> None:
11 | """Chatを削除する"""
12 |
13 |
14 | class DeleteChatUseCaseImpl(DeleteChatUseCase):
15 | def __init__(self, chat_repository: ChatRepository) -> None:
16 | self.chat_repository = chat_repository
17 |
18 | def execute(self, chat_id: ChatId) -> None:
19 | """Chatを削除する"""
20 | chat = self.chat_repository.find_by_id(chat_id)
21 | if chat is None:
22 | raise ChatNotFoundError(f"Chat with ID {chat_id.value} not found")
23 |
24 | self.chat_repository.delete(chat_id)
25 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/fileUtils.ts:
--------------------------------------------------------------------------------
1 | export function readBlob(fn: (reader: FileReader) => void) {
2 | return new Promise((resolve) => {
3 | const reader = new FileReader()
4 | reader.addEventListener('load', () => {
5 | resolve(reader.result as string)
6 | })
7 | fn(reader)
8 | })
9 | }
10 |
11 | export async function toDataUrl(url: string) {
12 | const res = await fetch(url)
13 | const buffer = await res.blob()
14 | return readBlob((r) => r.readAsDataURL(buffer))
15 | }
16 |
17 | export async function fileToBase64(file: File): Promise {
18 | return new Promise((resolve, reject) => {
19 | const reader = new FileReader()
20 | reader.readAsDataURL(file)
21 | reader.onload = () => {
22 | const result = reader.result as string
23 | const base64 = result.split(',')[1]
24 | resolve(base64 || '')
25 | }
26 | reader.onerror = (error) => reject(error)
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useIntermediateChatKeyword.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | import { useReaderSnapshot, reader } from '../models'
4 |
5 | // When inputting with IME and storing state in `valtio`,
6 | // unexpected rendering with `e.target.value === ''` occurs,
7 | // which leads to ` ` and IME flash to empty,
8 | // while this will not happen when using `React.useState`,
9 |
10 | // so we should create an intermediate `chatKeyword` state to fix this.
11 | export function useIntermediateChatKeyword() {
12 | const [chatKeyword, setChatKeyword] = useState('')
13 | const { focusedBookTab } = useReaderSnapshot()
14 |
15 | useEffect(() => {
16 | setChatKeyword(focusedBookTab?.chatKeyword ?? '')
17 | }, [focusedBookTab?.chatKeyword])
18 |
19 | useEffect(() => {
20 | reader.focusedBookTab?.setChatKeyword(chatKeyword)
21 | }, [chatKeyword])
22 |
23 | return [chatKeyword, setChatKeyword] as const
24 | }
25 |
--------------------------------------------------------------------------------
/apps/reader/src/components/base/ActionBar.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import { ComponentProps } from 'react'
3 | import { IconType } from 'react-icons'
4 |
5 | import { IconButton } from '../Button'
6 |
7 | export interface Action {
8 | id: string
9 | title: string
10 | Icon: IconType
11 | handle: () => void
12 | }
13 |
14 | interface ActionBarProps extends ComponentProps<'ul'> {
15 | actions: Action[]
16 | }
17 | export const ActionBar: React.FC = ({ actions, className }) => {
18 | return (
19 |
20 | {actions.map(({ id, title, Icon, handle }) => (
21 |
22 | {
25 | e.stopPropagation()
26 | handle()
27 | }}
28 | />
29 |
30 | ))}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useTranslation.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 | import { useCallback } from 'react'
3 |
4 | import locales from '../../locales'
5 |
6 | export function useTranslation(scope?: string) {
7 | const { locale = 'en-US' } = useRouter()
8 |
9 | return useCallback(
10 | (key: string, params?: Record) => {
11 | const localeData =
12 | locale in locales
13 | ? (locales as any)[locale]
14 | : (locales as any)['en-US'] || {}
15 |
16 | const translationKey = scope ? `${scope}.${key}` : key
17 | let text = translationKey in localeData ? localeData[translationKey] : key
18 |
19 | if (params && text) {
20 | Object.entries(params).forEach(([paramKey, value]) => {
21 | text = text.replace(new RegExp(`{${paramKey}}`, 'g'), String(value))
22 | })
23 | }
24 |
25 | return text
26 | },
27 | [locale, scope],
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useSWR/usePodcast.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 |
3 | import { components } from '../../lib/openapi-schema/schema'
4 |
5 | import { fetcher } from './fetcher'
6 |
7 | type PodcastListResponse = components['schemas']['PodcastListResponse']
8 |
9 | /**
10 | * Hook to fetch podcasts for a specific book
11 | * @param bookId The ID of the book to get podcasts for
12 | * @returns SWR hook result with podcasts data
13 | */
14 | export function usePodcastsByBook(bookId: string | undefined) {
15 | const { data, error, mutate } = useSWR(
16 | bookId
17 | ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/podcasts/book/${bookId}`
18 | : null,
19 | fetcher,
20 | {
21 | revalidateOnFocus: false,
22 | revalidateOnReconnect: false,
23 | },
24 | )
25 |
26 | return {
27 | podcasts: data?.podcasts || [],
28 | isLoading: !error && !data,
29 | error,
30 | mutate,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/apps/reader/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps, forwardRef } from 'react'
2 | import { IconType } from 'react-icons'
3 |
4 | import { cn } from '../lib/utils'
5 |
6 | import { Button as ShadcnButton } from './ui/button'
7 |
8 | // shadcn-uiのButtonを再エクスポートして既存コードとの互換性を保つ
9 | export const Button = ShadcnButton
10 |
11 | // IconButtonをshadcn-uiのButtonを使って実装
12 | interface IconButtonProps extends Omit, 'size'> {
13 | Icon: IconType
14 | size?: number
15 | }
16 |
17 | export const IconButton = forwardRef(
18 | ({ className, Icon, size = 16, ...props }, ref) => {
19 | return (
20 |
27 |
28 |
29 | )
30 | },
31 | )
32 |
33 | IconButton.displayName = 'IconButton'
34 |
--------------------------------------------------------------------------------
/apps/api/src/config/db.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from collections.abc import Generator
3 |
4 | from sqlalchemy import create_engine
5 | from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
6 |
7 | from src.config.app_config import AppConfig
8 |
9 | config = AppConfig.get_config()
10 |
11 |
12 | engine = create_engine(config.database_url, echo=True)
13 |
14 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
15 |
16 |
17 | class Base(DeclarativeBase):
18 | pass
19 |
20 |
21 | def get_db() -> Generator[Session]:
22 | db = SessionLocal()
23 | try:
24 | yield db
25 | finally:
26 | db.close()
27 |
28 |
29 | def init_db() -> None:
30 | try:
31 | Base.metadata.create_all(bind=engine)
32 | logging.info("Database tables initialized successfully")
33 | except Exception as e:
34 | logging.error(f"Error occurred during database initialization: {str(e)}", exc_info=True)
35 | raise
36 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/chat/find_chat_by_id_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.chat.entities.chat import Chat
4 | from src.domain.chat.exceptions.chat_exceptions import ChatNotFoundError
5 | from src.domain.chat.repositories.chat_repository import ChatRepository
6 | from src.domain.chat.value_objects.chat_id import ChatId
7 |
8 |
9 | class FindChatByIdUseCase(ABC):
10 | @abstractmethod
11 | def execute(self, chat_id: ChatId) -> Chat:
12 | """IDでChatを検索する"""
13 |
14 |
15 | class FindChatByIdUseCaseImpl(FindChatByIdUseCase):
16 | def __init__(self, chat_repository: ChatRepository) -> None:
17 | self.chat_repository = chat_repository
18 |
19 | def execute(self, chat_id: ChatId) -> Chat:
20 | """IDでChatを検索する"""
21 | chat = self.chat_repository.find_by_id(chat_id)
22 | if chat is None:
23 | raise ChatNotFoundError(f"Chat with ID {chat_id.value} not found")
24 | return chat
25 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/chat/find_chats_by_user_id_and_book_id_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.chat.entities.chat import Chat
4 | from src.domain.chat.repositories.chat_repository import ChatRepository
5 | from src.domain.chat.value_objects.book_id import BookId
6 | from src.domain.chat.value_objects.user_id import UserId
7 |
8 |
9 | class FindChatsByUserIdAndBookIdUseCase(ABC):
10 | @abstractmethod
11 | def execute(self, user_id: UserId, book_id: BookId) -> list[Chat]:
12 | """ユーザーIDと本IDに紐づくChatをすべて取得する."""
13 |
14 |
15 | class FindChatsByUserIdAndBookIdUseCaseImpl(FindChatsByUserIdAndBookIdUseCase):
16 | def __init__(self, chat_repository: ChatRepository) -> None:
17 | self.chat_repository = chat_repository
18 |
19 | def execute(self, user_id: UserId, book_id: BookId) -> list[Chat]:
20 | """ユーザーIDと本IDに紐づくChatをすべて取得する."""
21 | return self.chat_repository.find_by_user_id_and_book_id(user_id, book_id)
22 |
--------------------------------------------------------------------------------
/apps/reader/src/models/tree.ts:
--------------------------------------------------------------------------------
1 | export interface INode {
2 | id: string
3 | depth?: number
4 | expanded?: boolean
5 | subitems?: INode[]
6 | }
7 |
8 | export function flatTree(node: T, depth = 1): T[] {
9 | if (!node.subitems || !node.subitems.length || !node.expanded) {
10 | return [{ ...node, depth }]
11 | }
12 | const children = node.subitems.flatMap((i) => flatTree(i, depth + 1)) as T[]
13 | return [{ ...node, depth }, ...children]
14 | }
15 |
16 | export function find(
17 | nodes: T[] = [],
18 | id: string,
19 | ): T | undefined {
20 | const node = nodes.find((n) => n.id === id)
21 | if (node) return node
22 | for (const child of nodes) {
23 | const node = find(child.subitems, id)
24 | if (node) return node as T
25 | }
26 | return undefined
27 | }
28 |
29 | export function dfs(node: T, fn: (node: T) => void) {
30 | fn(node)
31 | node.subitems?.forEach((child) => dfs(child as T, fn))
32 | }
33 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/value_objects/sender_type.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from enum import Enum
3 |
4 |
5 | class SenderTypeEnum(Enum):
6 | USER = "user"
7 | ASSISTANT = "assistant"
8 |
9 |
10 | @dataclass(frozen=True)
11 | class SenderType:
12 | value: str
13 |
14 | def __post_init__(self) -> None:
15 | if not self.value:
16 | raise ValueError("SenderType value cannot be empty")
17 |
18 | valid_values = [e.value for e in SenderTypeEnum]
19 | if self.value not in valid_values:
20 | raise ValueError(f"Invalid sender type: {self.value}")
21 |
22 | @classmethod
23 | def user(cls) -> "SenderType":
24 | return cls(SenderTypeEnum.USER.value)
25 |
26 | @classmethod
27 | def assistant(cls) -> "SenderType":
28 | return cls(SenderTypeEnum.ASSISTANT.value)
29 |
30 | @classmethod
31 | def from_string(cls, sender_type_str: str) -> "SenderType":
32 | return cls(sender_type_str)
33 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export function keys(o: T) {
2 | return Object.keys(o) as (keyof T)[]
3 | }
4 |
5 | export function clamp(value: number, min: number, max: number) {
6 | return Math.min(Math.max(value, min), max)
7 | }
8 |
9 | export function last(array: T[]) {
10 | return array[array.length - 1]
11 | }
12 |
13 | export function group(array: T[], getKey: (item: T) => string | number) {
14 | const o: Record = {}
15 |
16 | array.forEach((item) => {
17 | const key = getKey(item)
18 | o[key] = [...(o[key] ?? []), item]
19 | })
20 |
21 | return o
22 | }
23 |
24 | export function copy(text: string) {
25 | return navigator.clipboard.writeText(text)
26 | }
27 |
28 | export function cleanUpText(str: string, strict = false): string {
29 | str = str.replace(/^\s+|\s+$/g, '')
30 | if (strict) {
31 | str = str.replace(/\n{2,}/g, '\n')
32 | }
33 | return str
34 | }
35 |
36 | export const IS_SERVER = typeof window === 'undefined'
37 |
--------------------------------------------------------------------------------
/apps/reader/src/utils/annotation.ts:
--------------------------------------------------------------------------------
1 | export type AnnotationType = keyof typeof typeMap
2 |
3 | export const typeMap = {
4 | highlight: {
5 | style: 'backgroundColor',
6 | class: 'rounded',
7 | },
8 | // underline: {
9 | // style: 'border-bottom-color',
10 | // class: 'border-b-2',
11 | // },
12 | }
13 |
14 | export type AnnotationColor = keyof typeof colorMap
15 |
16 | // "dark color + low opacity" is clearer than "light color + high opacity"
17 | // from tailwind [color]-600
18 | export const colorMap = {
19 | yellow: 'rgba(217, 119, 6, 0.2)',
20 | red: 'rgba(220, 38, 38, 0.2)',
21 | green: 'rgba(22, 163, 74, 0.2)',
22 | blue: 'rgba(37, 99, 235, 0.2)',
23 | }
24 |
25 | export interface Annotation {
26 | id: string
27 | bookId: string
28 | cfi: string
29 | spine: {
30 | index: number
31 | title: string
32 | }
33 | createAt: number
34 | updatedAt: number
35 | type: AnnotationType
36 | color: AnnotationColor
37 | notes?: string
38 | text: string
39 | }
40 |
--------------------------------------------------------------------------------
/packages/epubjs/test/locations.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 |
3 | import Locations from '../src/locations'
4 | import * as core from '../src/utils/core'
5 |
6 | describe('Locations', function () {
7 | describe('#parse', function () {
8 | var chapter = require('./fixtures/locations.xhtml').default
9 |
10 | it('parse locations from a document', function () {
11 | var doc = core.parse(chapter, 'application/xhtml+xml')
12 | var contents = doc.documentElement
13 | var locations = new Locations()
14 | var result = locations.parse(contents, '/6/4[chap01ref]', 100)
15 | assert.equal(result.length, 15)
16 | })
17 |
18 | it('parse locations from xmldom', function () {
19 | var doc = core.parse(chapter, 'application/xhtml+xml', true)
20 | var contents = doc.documentElement
21 |
22 | var locations = new Locations()
23 | var result = locations.parse(contents, '/6/4[chap01ref]', 100)
24 | assert.equal(result.length, 15)
25 | })
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from 'next-themes'
2 | import { Toaster as Sonner } from 'sonner'
3 |
4 | type ToasterProps = React.ComponentProps
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = 'system' } = useTheme()
8 |
9 | return (
10 |
26 | )
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/packages/epubjs/types/themes.d.ts:
--------------------------------------------------------------------------------
1 | import Contents from './contents'
2 | import { Rendition } from './rendition'
3 |
4 | export default class Themes {
5 | constructor(rendition: Rendition)
6 |
7 | register(themeObject: object): void
8 |
9 | register(theme: string, url: string): void
10 |
11 | register(theme: string, themeObject: object): void
12 |
13 | default(theme: object | string): void
14 |
15 | registerThemes(themes: object): void
16 |
17 | registerCss(name: string, css: string): void
18 |
19 | registerUrl(name: string, input: string): void
20 |
21 | registerRules(name: string, rules: object): void
22 |
23 | select(name: string): void
24 |
25 | update(name: string): void
26 |
27 | inject(content: Contents): void
28 |
29 | add(name: string, contents: Contents): void
30 |
31 | override(name: string, value: string, priority?: boolean): void
32 |
33 | overrides(contents: Contents): void
34 |
35 | fontSize(size: string): void
36 |
37 | font(f: string): void
38 |
39 | destroy(): void
40 | }
41 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/postgres/db_util.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from sqlalchemy import DateTime, func
4 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
5 |
6 |
7 | class Base(DeclarativeBase):
8 | """Base class for declarative SQLAlchemy models"""
9 |
10 | # Include default repr for easier debugging
11 | def __repr__(self) -> str: # pragma: no cover
12 | attrs = " ".join(f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_"))
13 | return f"<{self.__class__.__name__} {attrs}>"
14 |
15 |
16 | class TimestampMixin:
17 | """Mixin class to add created_at and updated_at timestamps to SQLAlchemy models."""
18 |
19 | created_at: Mapped[datetime] = mapped_column(
20 | DateTime,
21 | server_default=func.now(),
22 | nullable=False,
23 | )
24 | updated_at: Mapped[datetime] = mapped_column(
25 | DateTime,
26 | server_default=func.now(),
27 | onupdate=func.now(),
28 | nullable=False,
29 | )
30 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/value_objects/annotation_type.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from enum import Enum
3 |
4 |
5 | class AnnotationTypeEnum(Enum):
6 | HIGHLIGHT = "highlight"
7 |
8 |
9 | @dataclass(frozen=True)
10 | class AnnotationType:
11 | value: str
12 |
13 | def __post_init__(self) -> None:
14 | if not any(self.value == type_.value for type_ in AnnotationTypeEnum):
15 | raise ValueError(f"Invalid annotation type: {self.value}")
16 |
17 | @classmethod
18 | def highlight(cls) -> "AnnotationType":
19 | """ハイライトタイプのアノテーションを作成"""
20 | return cls(AnnotationTypeEnum.HIGHLIGHT.value)
21 |
22 | @classmethod
23 | def default(cls) -> "AnnotationType":
24 | """デフォルトタイプのアノテーションを作成"""
25 | return cls.highlight()
26 |
27 | @classmethod
28 | def from_string(cls, type_str: str | None) -> "AnnotationType":
29 | """文字列からAnnotationTypeを生成"""
30 | if not type_str:
31 | return cls.default()
32 | return cls(type_str)
33 |
--------------------------------------------------------------------------------
/packages/epubjs/src/epub.js:
--------------------------------------------------------------------------------
1 | import Book from './book'
2 | import Contents from './contents'
3 | import CFI from './epubcfi'
4 | import ContinuousViewManager from './managers/continuous'
5 | import DefaultViewManager from './managers/default'
6 | import IframeView from './managers/views/iframe'
7 | import Rendition from './rendition'
8 | import { EPUBJS_VERSION } from './utils/constants'
9 | import * as utils from './utils/core'
10 |
11 | /**
12 | * Creates a new Book
13 | * @param {string|ArrayBuffer} url URL, Path or ArrayBuffer
14 | * @param {object} options to pass to the book
15 | * @returns {Book} a new Book object
16 | * @example ePub("/path/to/book.epub", {})
17 | */
18 | function ePub(url, options) {
19 | return new Book(url, options)
20 | }
21 |
22 | ePub.VERSION = EPUBJS_VERSION
23 |
24 | if (typeof global !== 'undefined') {
25 | global.EPUBJS_VERSION = EPUBJS_VERSION
26 | }
27 |
28 | ePub.Book = Book
29 | ePub.Rendition = Rendition
30 | ePub.Contents = Contents
31 | ePub.CFI = CFI
32 | ePub.utils = utils
33 |
34 | export default ePub
35 |
--------------------------------------------------------------------------------
/apps/reader/src/components/podcast/PodcastPane.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | import { usePodcastSelection } from '../../hooks/podcast/usePodcastSelection'
4 | import { usePodcastsByBook } from '../../hooks/useSWR/usePodcast'
5 | import { components } from '../../lib/openapi-schema/schema'
6 | import { useReaderSnapshot } from '../../models'
7 |
8 | import { LibraryPodcastView } from './LibraryPodcastView'
9 | import { PodcastDetail } from './PodcastDetail'
10 |
11 | export const PodcastPane: React.FC = memo(() => {
12 | const { focusedBookTab } = useReaderSnapshot()
13 | const { podcasts } = usePodcastsByBook(focusedBookTab?.book.id)
14 | const { selectedPodcast } = usePodcastSelection({
15 | podcasts,
16 | bookId: focusedBookTab?.book.id,
17 | })
18 |
19 | if (!focusedBookTab) {
20 | return
21 | }
22 |
23 | return (
24 |
28 | )
29 | })
30 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/chat/create_chat_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.chat.entities.chat import Chat
4 | from src.domain.chat.repositories.chat_repository import ChatRepository
5 | from src.domain.chat.value_objects.book_id import BookId
6 | from src.domain.chat.value_objects.chat_title import ChatTitle
7 | from src.domain.chat.value_objects.user_id import UserId
8 |
9 |
10 | class CreateChatUseCase(ABC):
11 | @abstractmethod
12 | def execute(self, user_id: UserId, title: ChatTitle, book_id: BookId | None = None) -> Chat:
13 | """新しいChatを作成する."""
14 |
15 |
16 | class CreateChatUseCaseImpl(CreateChatUseCase):
17 | def __init__(self, chat_repository: ChatRepository) -> None:
18 | self.chat_repository = chat_repository
19 |
20 | def execute(self, user_id: UserId, title: ChatTitle, book_id: BookId | None = None) -> Chat:
21 | """新しいChatを作成する."""
22 | chat = Chat.create(user_id=user_id, title=title, book_id=book_id)
23 | self.chat_repository.save(chat)
24 | return chat
25 |
--------------------------------------------------------------------------------
/apps/reader/src/components/chat/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { useTranslation } from '@flow/reader/hooks'
4 |
5 | export const EmptyState: React.FC = () => {
6 | const t = useTranslation()
7 |
8 | return (
9 |
10 |
25 |
{t('chat.empty_title')}
26 |
{t('chat.empty_description')}
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/packages/epubjs/types/resources.d.ts:
--------------------------------------------------------------------------------
1 | import Archive from './archive'
2 | import { PackagingManifestObject } from './packaging'
3 |
4 | export default class Resources {
5 | constructor(
6 | manifest: PackagingManifestObject,
7 | options: {
8 | replacements?: string
9 | archive?: Archive
10 | resolver?: Function
11 | request?: Function
12 | },
13 | )
14 |
15 | process(manifest: PackagingManifestObject): void
16 |
17 | createUrl(url: string): Promise
18 |
19 | replacements(): Promise>
20 |
21 | replacementUrls: string[]
22 | assets: any[]
23 |
24 | relativeTo(absolute: boolean, resolver?: Function): Array
25 |
26 | get(path: string): string
27 |
28 | substitute(content: string, url?: string): string
29 |
30 | destroy(): void
31 |
32 | private split(): void
33 |
34 | private splitUrls(): void
35 |
36 | private replaceCss(
37 | archive: Archive,
38 | resolver?: Function,
39 | ): Promise>
40 |
41 | private createCssFile(href: string): Promise
42 | }
43 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/external/epub/epub_reader.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass
3 |
4 | from bs4 import BeautifulSoup
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | @dataclass
10 | class Chapter:
11 | """Represents a book chapter"""
12 |
13 | index: int
14 | title: str | None
15 | content: str
16 |
17 | def get_text_content(self) -> str:
18 | """Get plain text content from HTML"""
19 | soup = BeautifulSoup(self.content, "html.parser")
20 | # Remove script and style elements
21 | for script in soup(["script", "style"]):
22 | script.decompose()
23 | # Get text
24 | text = soup.get_text()
25 | # Break into lines and remove leading and trailing space on each
26 | lines = (line.strip() for line in text.splitlines())
27 | # Break multi-headlines into a line each
28 | chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
29 | # Drop blank lines
30 | return "\n".join(chunk for chunk in chunks if chunk)
31 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/book/find_books_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.book.entities.book import Book
4 | from src.domain.book.repositories.book_repository import BookRepository
5 |
6 |
7 | class FindBooksUseCase(ABC):
8 | @abstractmethod
9 | def execute(self) -> list[Book]:
10 | pass
11 |
12 |
13 | class FindBooksUseCaseImpl(FindBooksUseCase):
14 | def __init__(self, book_repository: BookRepository) -> None:
15 | self.book_repository = book_repository
16 |
17 | def execute(self) -> list[Book]:
18 | return self.book_repository.find_all()
19 |
20 |
21 | class FindBooksByUserIdUseCase(ABC):
22 | @abstractmethod
23 | def execute(self, user_id: str) -> list[Book]:
24 | pass
25 |
26 |
27 | class FindBooksByUserIdUseCaseImpl(FindBooksByUserIdUseCase):
28 | def __init__(self, book_repository: BookRepository) -> None:
29 | self.book_repository = book_repository
30 |
31 | def execute(self, user_id: str) -> list[Book]:
32 | return self.book_repository.find_by_user_id(user_id)
33 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/chat/update_chat_title_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.chat.exceptions.chat_exceptions import ChatNotFoundError
4 | from src.domain.chat.repositories.chat_repository import ChatRepository
5 | from src.domain.chat.value_objects.chat_id import ChatId
6 | from src.domain.chat.value_objects.chat_title import ChatTitle
7 |
8 |
9 | class UpdateChatTitleUseCase(ABC):
10 | @abstractmethod
11 | def execute(self, chat_id: ChatId, title: ChatTitle) -> None:
12 | """Chatのタイトルを更新する"""
13 |
14 |
15 | class UpdateChatTitleUseCaseImpl(UpdateChatTitleUseCase):
16 | def __init__(self, chat_repository: ChatRepository) -> None:
17 | self.chat_repository = chat_repository
18 |
19 | def execute(self, chat_id: ChatId, title: ChatTitle) -> None:
20 | """Chatのタイトルを更新する"""
21 | chat = self.chat_repository.find_by_id(chat_id)
22 | if chat is None:
23 | raise ChatNotFoundError(f"Chat with ID {chat_id.value} not found")
24 |
25 | chat.update_title(title)
26 | self.chat_repository.save(chat)
27 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/schemas/chat_schema.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from pydantic import Field
4 |
5 | from src.presentation.api.schemas.base_schema import BaseSchemaModel
6 |
7 |
8 | class ChatCreateRequest(BaseSchemaModel):
9 | user_id: str = Field(..., description="ユーザーID")
10 | title: str | None = Field(None, description="チャットのタイトル", max_length=255)
11 | book_id: str | None = Field(None, description="関連する本のID")
12 |
13 |
14 | class ChatUpdateTitleRequest(BaseSchemaModel):
15 | title: str = Field(..., description="更新するチャットのタイトル", max_length=255)
16 |
17 |
18 | class ChatResponse(BaseSchemaModel):
19 | id: str = Field(..., description="チャットID")
20 | user_id: str = Field(..., description="ユーザーID")
21 | title: str = Field(..., description="チャットのタイトル")
22 | book_id: str | None = Field(None, description="関連する本のID")
23 | created_at: datetime = Field(..., description="作成日時")
24 | updated_at: datetime = Field(..., description="更新日時")
25 |
26 |
27 | class ChatsResponse(BaseSchemaModel):
28 | chats: list[ChatResponse] = Field(..., description="チャットリスト")
29 |
--------------------------------------------------------------------------------
/packages/epubjs/types/mapping.d.ts:
--------------------------------------------------------------------------------
1 | import Contents from './contents'
2 | import Layout from './layout'
3 |
4 | export interface EpubCFIPair {
5 | start: string
6 | end: string
7 | }
8 |
9 | export interface RangePair {
10 | start: Range
11 | end: Range
12 | }
13 |
14 | export default class Mapping {
15 | constructor(layout: Layout, direction?: string, axis?: string, dev?: boolean)
16 |
17 | page(
18 | contents: Contents,
19 | cfiBase: string,
20 | start: number,
21 | end: number,
22 | ): EpubCFIPair
23 |
24 | axis(axis: string): boolean
25 |
26 | private walk(root: Node, func: Function)
27 |
28 | private findStart(root: Node, start: number, end: number): Range
29 |
30 | private findEnd(root: Node, start: number, end: number): Range
31 |
32 | private findTextStartRange(node: Node, start: number, end: number): Range
33 |
34 | private findTextEndRange(node: Node, start: number, end: number): Range
35 |
36 | private splitTextNodeIntoRanges(node: Node, _splitter?: string): Array
37 |
38 | private rangePairToCfiPair(cfiBase: string, rangePair: RangePair): EpubCFIPair
39 | }
40 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@flow/reader/lib/utils'
4 |
5 | interface CircularProgressProps extends React.HTMLAttributes {
6 | size?: 'sm' | 'md' | 'lg'
7 | thickness?: 'thin' | 'normal' | 'thick'
8 | }
9 |
10 | const CircularProgress = React.forwardRef<
11 | HTMLDivElement,
12 | CircularProgressProps
13 | >(({ className, size = 'md', thickness = 'normal', ...props }, ref) => {
14 | const sizeClasses = {
15 | sm: 'h-4 w-4',
16 | md: 'h-8 w-8',
17 | lg: 'h-12 w-12',
18 | }
19 |
20 | const thicknessClasses = {
21 | thin: 'border-2',
22 | normal: 'border-[3px]',
23 | thick: 'border-4',
24 | }
25 |
26 | return (
27 |
37 | )
38 | })
39 |
40 | CircularProgress.displayName = 'CircularProgress'
41 |
42 | export { CircularProgress }
43 |
--------------------------------------------------------------------------------
/packages/epubjs/types/locations.d.ts:
--------------------------------------------------------------------------------
1 | import EpubCFI from './epubcfi'
2 | import Section from './section'
3 | import Spine from './spine'
4 |
5 | export default class Locations {
6 | constructor(spine: Spine, request?: Function, pause?: number)
7 |
8 | generate(chars: number): Promise>
9 |
10 | process(section: Section): Promise>
11 |
12 | locationFromCfi(cfi: string | EpubCFI): Location
13 |
14 | percentageFromCfi(cfi: string | EpubCFI): number
15 |
16 | percentageFromLocation(loc: number): number
17 |
18 | cfiFromLocation(loc: number): string
19 |
20 | cfiFromPercentage(percentage: number): string
21 |
22 | load(locations: string): Array
23 |
24 | save(): string
25 |
26 | currentLocation(): Location
27 | currentLocation(curr: string | number): void
28 |
29 | length(): number
30 |
31 | destroy(): void
32 |
33 | private createRange(): {
34 | startContainer: Element
35 | startOffset: number
36 | endContainer: Element
37 | endOffset: number
38 | }
39 |
40 | private parse(contents: Node, cfiBase: string, chars: number): Array
41 | }
42 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useLoading.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import {
4 | useLoadingState,
5 | useProgressManager,
6 | useTaskManager,
7 | UseTaskManagerOptions,
8 | } from './loading'
9 |
10 | export type UseLoadingOptions = UseTaskManagerOptions
11 |
12 | /**
13 | * ローディング状態を管理するための統合フック
14 | * 後方互換性のために従来のAPIを維持します
15 | */
16 | export function useLoading(options?: UseLoadingOptions) {
17 | const currentTaskIdRef = useRef(null)
18 |
19 | const getCurrentTaskId = () => currentTaskIdRef.current
20 | const setCurrentTaskId = (id: string | null) => {
21 | currentTaskIdRef.current = id
22 | }
23 |
24 | const { isLoading, isGlobalLoading } = useLoadingState(getCurrentTaskId)
25 | const { updateProgress, updateSubTasks } =
26 | useProgressManager(getCurrentTaskId)
27 | const { startLoading, stopLoading } = useTaskManager(
28 | setCurrentTaskId,
29 | getCurrentTaskId,
30 | options,
31 | )
32 |
33 | return {
34 | startLoading,
35 | updateProgress,
36 | updateSubTasks,
37 | stopLoading,
38 | isLoading,
39 | isGlobalLoading,
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useLongPress.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 |
3 | import { LONG_PRESS } from '../constants/audio'
4 |
5 | interface LongPressOptions {
6 | interval?: number
7 | }
8 |
9 | interface LongPressHandlers {
10 | onPointerDown: () => void
11 | onPointerUp: () => void
12 | onPointerLeave: () => void
13 | }
14 |
15 | /**
16 | * 長押し操作を処理するカスタムフック
17 | * @param action 実行する関数
18 | * @param options オプション設定
19 | * @returns イベントハンドラー
20 | */
21 | export const useLongPress = (
22 | action: () => void,
23 | options: LongPressOptions = {},
24 | ): LongPressHandlers => {
25 | const timer = useRef(null)
26 | const { interval = LONG_PRESS.DEFAULT_INTERVAL } = options
27 |
28 | const start = () => {
29 | // 最初に一度実行
30 | action()
31 | // インターバルで継続実行
32 | timer.current = setInterval(action, interval)
33 | }
34 |
35 | const stop = () => {
36 | if (timer.current) {
37 | clearInterval(timer.current)
38 | timer.current = null
39 | }
40 | }
41 |
42 | return {
43 | onPointerDown: start,
44 | onPointerUp: stop,
45 | onPointerLeave: stop,
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Bug Report
2 | description: Report an issue that should be fixed
3 | labels: [triage]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for submitting a bug report! It helps make BookWith better.
9 |
10 | Please include as much information as possible.
11 | - type: input
12 | attributes:
13 | label: Which browser/platform are you running the app on? (with version if possible)
14 | placeholder: Chrome 115, Safari 16, Firefox 118, etc.
15 | validations:
16 | required: true
17 | - type: textarea
18 | attributes:
19 | label: What steps can reproduce the bug?
20 | description: Describe the bug and provide steps or code that can reproduce it.
21 | validations:
22 | required: true
23 | - type: textarea
24 | attributes:
25 | label: What is the expected behavior?
26 | - type: textarea
27 | attributes:
28 | label: What do you see instead?
29 | - type: textarea
30 | attributes:
31 | label: Additional information
32 | description: Is there anything else you think we should know?
33 |
--------------------------------------------------------------------------------
/apps/api/src/domain/podcast/value_objects/speaker_role.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass
4 | from enum import Enum
5 |
6 |
7 | class SpeakerRoleEnum(str, Enum):
8 | HOST = "HOST"
9 | GUEST = "GUEST"
10 |
11 |
12 | @dataclass(frozen=True)
13 | class SpeakerRole:
14 | value: SpeakerRoleEnum
15 |
16 | def __post_init__(self) -> None:
17 | if not isinstance(self.value, SpeakerRoleEnum):
18 | raise ValueError("SpeakerRole must be a SpeakerRoleEnum instance")
19 |
20 | def __str__(self) -> str:
21 | return self.value.value
22 |
23 | @classmethod
24 | def host(cls) -> SpeakerRole:
25 | return cls(SpeakerRoleEnum.HOST)
26 |
27 | @classmethod
28 | def guest(cls) -> SpeakerRole:
29 | return cls(SpeakerRoleEnum.GUEST)
30 |
31 | @classmethod
32 | def from_string(cls, value: str) -> SpeakerRole:
33 | return cls(SpeakerRoleEnum(value))
34 |
35 | def is_host(self) -> bool:
36 | return self.value == SpeakerRoleEnum.HOST
37 |
38 | def is_guest(self) -> bool:
39 | return self.value == SpeakerRoleEnum.GUEST
40 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/message/find_message_by_id_usecase.py:
--------------------------------------------------------------------------------
1 | """メッセージをIDで検索するユースケース"""
2 |
3 | from abc import ABC, abstractmethod
4 |
5 | from src.domain.message.entities.message import Message
6 | from src.domain.message.exceptions.message_exceptions import MessageNotFoundException
7 | from src.domain.message.repositories.message_repository import MessageRepository
8 | from src.domain.message.value_objects.message_id import MessageId
9 |
10 |
11 | class FindMessageByIdUseCase(ABC):
12 | @abstractmethod
13 | def execute(self, message_id: str) -> Message:
14 | """メッセージをIDで検索する"""
15 |
16 |
17 | class FindMessageByIdUseCaseImpl(FindMessageByIdUseCase):
18 | def __init__(self, message_repository: MessageRepository) -> None:
19 | self.message_repository = message_repository
20 |
21 | def execute(self, message_id: str) -> Message:
22 | """メッセージをIDで検索し、見つからない場合は例外をスローする"""
23 | message_id_obj = MessageId(message_id)
24 | message = self.message_repository.find_by_id(message_id_obj)
25 |
26 | if message is None:
27 | raise MessageNotFoundException(message_id)
28 |
29 | return message
30 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/error_messages/book_error_message.py:
--------------------------------------------------------------------------------
1 | # 一般的なエラーメッセージ
2 | BOOK_NOT_FOUND = "指定されたIDの書籍が見つかりません。"
3 | BOOK_ACCESS_DENIED = "この書籍へのアクセス権限がありません。"
4 | BOOK_FILE_NOT_FOUND = "この書籍のファイルが見つかりません。"
5 | BOOK_COVER_NOT_FOUND = "この書籍のカバー画像が見つかりません。"
6 | BOOK_ALREADY_STARTED = "この書籍は既に読書開始状態です。"
7 | BOOK_ALREADY_COMPLETED = "この書籍は既に読了済みです。"
8 |
9 | # 操作エラーメッセージ
10 | BOOK_CREATE_ERROR = "書籍の作成中にエラーが発生しました: {error}"
11 | BOOK_UPDATE_ERROR = "書籍の更新中にエラーが発生しました: {error}"
12 | BOOK_DELETE_ERROR = "書籍の削除中にエラーが発生しました: {error}"
13 | BOOK_BULK_DELETE_ERROR = "書籍の一括削除中にエラーが発生しました: {error}"
14 | BOOK_FETCH_ERROR = "書籍の取得中にエラーが発生しました: {error}"
15 | BOOK_COVER_FETCH_ERROR = "書籍カバー画像の取得中にエラーが発生しました: {error}"
16 | BOOK_FILE_FETCH_ERROR = "書籍ファイルの取得中にエラーが発生しました: {error}"
17 | SIGNED_URL_GENERATION_ERROR = "署名付きURLの生成中にエラーが発生しました: {error}"
18 |
19 | # 入力検証エラーメッセージ
20 | BOOK_TITLE_REQUIRED = "書籍のタイトルは必須です。"
21 | BOOK_TITLE_TOO_LONG = "書籍のタイトルは100文字以下である必要があります。"
22 | BOOK_DESCRIPTION_TOO_LONG = "書籍の説明は1000文字以下である必要があります。"
23 | BOOK_DATA_REQUIRED = "書籍データは必須です。"
24 | BOOK_USER_ID_REQUIRED = "ユーザーIDは必須です。"
25 | BOOK_ID_INVALID_FORMAT = "書籍IDの形式が無効です。有効なUUIDを指定してください。"
26 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/routes.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 |
3 | from src.presentation.api.handlers.annotation_api_route_handler import router as annotation_router
4 | from src.presentation.api.handlers.book_api_route_handler import router as book_router
5 | from src.presentation.api.handlers.chat_api_route_handler import router as chat_router
6 | from src.presentation.api.handlers.message_api_route_handler import router as message_router
7 | from src.presentation.api.handlers.podcast_api_route_handler import router as podcast_router
8 | from src.presentation.api.handlers.rag_api_route_handler import router as rag_router
9 |
10 |
11 | def setup_routes(app: FastAPI) -> None:
12 | app.include_router(message_router, prefix="/messages", tags=["messages"])
13 | app.include_router(rag_router, prefix="/rag", tags=["rag"])
14 | app.include_router(book_router, prefix="/books", tags=["books"])
15 | app.include_router(annotation_router, prefix="/books/{book_id}/annotations", tags=["annotations"])
16 | app.include_router(chat_router, prefix="/chats", tags=["chats"])
17 | app.include_router(podcast_router, prefix="/podcasts", tags=["podcasts"])
18 |
--------------------------------------------------------------------------------
/apps/reader/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import './styles.css'
4 | import 'react-photo-view/dist/react-photo-view.css'
5 |
6 | import { LiteralProvider } from '@literal-ui/core'
7 | import { ErrorBoundary } from '@sentry/nextjs'
8 | import type { AppProps } from 'next/app'
9 | import { useRouter } from 'next/router'
10 |
11 | import { Layout, Theme } from '../components'
12 | import { GlobalLoadingOverlay } from '../components/GlobalLoadingOverlay'
13 |
14 | export const TEST_USER_ID = '91527c9d-48aa-41d0-bb85-dc96f26556a0'
15 |
16 | export default function MyApp({ Component, pageProps }: AppProps) {
17 | const router = useRouter()
18 |
19 | if (router.pathname === '/success') return
20 |
21 | return (
22 | }>
23 | {/* @ts-ignore */}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | )
33 | }
34 |
35 | const Fallback: React.FC = () => {
36 | return Something went wrong.
37 | }
38 |
--------------------------------------------------------------------------------
/apps/api/src/domain/chat/repositories/chat_repository.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.chat.entities.chat import Chat
4 | from src.domain.chat.value_objects.book_id import BookId
5 | from src.domain.chat.value_objects.chat_id import ChatId
6 | from src.domain.chat.value_objects.user_id import UserId
7 |
8 |
9 | class ChatRepository(ABC):
10 | """Chatリポジトリのインターフェース"""
11 |
12 | @abstractmethod
13 | def save(self, chat: Chat) -> None:
14 | """Chatを保存する"""
15 |
16 | @abstractmethod
17 | def find_by_id(self, chat_id: ChatId) -> Chat | None:
18 | """IDでChatを検索する"""
19 |
20 | @abstractmethod
21 | def find_by_user_id(self, user_id: UserId) -> list[Chat]:
22 | """ユーザーIDに紐づくChatをすべて取得する"""
23 |
24 | @abstractmethod
25 | def find_by_book_id(self, book_id: BookId) -> list[Chat]:
26 | """本IDに紐づくChatをすべて取得する"""
27 |
28 | @abstractmethod
29 | def find_by_user_id_and_book_id(self, user_id: UserId, book_id: BookId) -> list[Chat]:
30 | """ユーザーIDと本IDに紐づくChatを検索する"""
31 |
32 | @abstractmethod
33 | def delete(self, chat_id: ChatId) -> None:
34 | """IDでChatを削除する"""
35 |
--------------------------------------------------------------------------------
/apps/reader/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json",
3 | "name": "Bookwith",
4 | "short_name": "Bookwith",
5 | "description": "Redefine EPUB reader",
6 | "icons": [
7 | {
8 | "src": "/icons/192.png",
9 | "type": "image/png",
10 | "sizes": "192x192"
11 | },
12 | {
13 | "src": "/icons/512.png",
14 | "type": "image/png",
15 | "sizes": "512x512"
16 | },
17 | {
18 | "src": "/icons/maskable-192.png",
19 | "type": "image/png",
20 | "sizes": "192x192",
21 | "purpose": "maskable"
22 | },
23 | {
24 | "src": "/icons/maskable-512.png",
25 | "type": "image/png",
26 | "sizes": "512x512",
27 | "purpose": "maskable"
28 | }
29 | ],
30 | "theme_color": "#fff",
31 | "background_color": "#fff",
32 | "start_url": "/",
33 | "display": "standalone",
34 | "orientation": "portrait",
35 | "file_handlers": [
36 | {
37 | "action": "/",
38 | "accept": {
39 | "application/epub+zip": ".epub",
40 | "application/epub": ".epub"
41 | },
42 | "launch_type": "single-client"
43 | }
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/exceptions/book_exceptions.py:
--------------------------------------------------------------------------------
1 | class BookDomainException(Exception): # noqa: N818
2 | """Bookドメインに関連する例外の基底クラス"""
3 |
4 |
5 | class BookNotFoundException(BookDomainException):
6 | """要求された書籍が見つからなかった場合の例外"""
7 |
8 | def __init__(self, book_id: str) -> None:
9 | self.book_id = book_id
10 | super().__init__(f"ID {book_id} の書籍が見つかりません")
11 |
12 |
13 | class BookAlreadyStartedException(BookDomainException):
14 | """既に開始されている書籍を再度開始しようとした場合の例外"""
15 |
16 | def __init__(self) -> None:
17 | super().__init__("この書籍は既に読書開始状態です")
18 |
19 |
20 | class BookAlreadyCompletedException(BookDomainException):
21 | """既に完了している書籍に対して操作を行おうとした場合の例外"""
22 |
23 | def __init__(self) -> None:
24 | super().__init__("この書籍は既に読了済みです")
25 |
26 |
27 | class BookPermissionDeniedException(BookDomainException):
28 | """ユーザーが書籍に対するアクセス権を持っていない場合の例外"""
29 |
30 | def __init__(self) -> None:
31 | super().__init__("この書籍へのアクセス権限がありません")
32 |
33 |
34 | class BookFileNotFoundException(BookDomainException):
35 | """書籍ファイルが見つからない場合の例外"""
36 |
37 | def __init__(self) -> None:
38 | super().__init__("この書籍のファイルが見つかりません")
39 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/book/find_book_by_id_usecase.py:
--------------------------------------------------------------------------------
1 | """特定の書籍を取得するユースケース"""
2 |
3 | from abc import ABC, abstractmethod
4 |
5 | from src.domain.book.entities.book import Book
6 | from src.domain.book.exceptions.book_exceptions import BookNotFoundException
7 | from src.domain.book.repositories.book_repository import BookRepository
8 | from src.domain.book.value_objects.book_id import BookId
9 |
10 |
11 | class FindBookByIdUseCase(ABC):
12 | """FindBookByIdUseCaseは、IDで特定の書籍を取得するためのユースケースインターフェースを定義する。"""
13 |
14 | @abstractmethod
15 | def execute(self, book_id: str) -> Book:
16 | """IDで特定の書籍を取得する"""
17 |
18 |
19 | class FindBookByIdUseCaseImpl(FindBookByIdUseCase):
20 | """FindBookByIdUseCaseImplは、IDで特定の書籍を取得するユースケース実装。"""
21 |
22 | def __init__(self, book_repository: BookRepository) -> None:
23 | self.book_repository = book_repository
24 |
25 | def execute(self, book_id: str) -> Book:
26 | """IDで特定の書籍を取得する。見つからない場合は例外を発生させる。"""
27 | book_id_obj = BookId(book_id)
28 | book = self.book_repository.find_by_id(book_id_obj)
29 |
30 | if book is None:
31 | raise BookNotFoundException(book_id)
32 |
33 | return book
34 |
--------------------------------------------------------------------------------
/apps/api/src/domain/annotation/value_objects/annotation_color.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from enum import Enum
3 |
4 |
5 | class AnnotationColorEnum(Enum):
6 | YELLOW = "yellow"
7 | RED = "red"
8 | GREEN = "green"
9 | BLUE = "blue"
10 |
11 |
12 | @dataclass(frozen=True)
13 | class AnnotationColor:
14 | value: str | None
15 |
16 | def __post_init__(self) -> None:
17 | if self.value is not None and not any(self.value == color.value for color in AnnotationColorEnum):
18 | raise ValueError(f"Invalid annotation color: {self.value}")
19 |
20 | @classmethod
21 | def yellow(cls) -> "AnnotationColor":
22 | return cls(AnnotationColorEnum.YELLOW.value)
23 |
24 | @classmethod
25 | def red(cls) -> "AnnotationColor":
26 | return cls(AnnotationColorEnum.RED.value)
27 |
28 | @classmethod
29 | def green(cls) -> "AnnotationColor":
30 | return cls(AnnotationColorEnum.GREEN.value)
31 |
32 | @classmethod
33 | def blue(cls) -> "AnnotationColor":
34 | return cls(AnnotationColorEnum.BLUE.value)
35 |
36 | @classmethod
37 | def from_string(cls, color_str: str | None) -> "AnnotationColor":
38 | return cls(color_str)
39 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/podcast/find_podcasts_by_book_id_usecase.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from src.domain.book.value_objects.book_id import BookId
4 | from src.domain.podcast.entities.podcast import Podcast
5 | from src.domain.podcast.repositories.podcast_repository import PodcastRepository
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | class FindPodcastsByBookIdUseCase:
11 | """Use case for finding podcasts by book ID"""
12 |
13 | def __init__(self, podcast_repository: PodcastRepository) -> None:
14 | self.podcast_repository = podcast_repository
15 |
16 | async def execute(self, book_id: BookId) -> list[Podcast]:
17 | """Find all podcasts for a specific book
18 |
19 | Args:
20 | book_id: ID of the book
21 |
22 | Returns:
23 | List of podcasts for the book
24 |
25 | """
26 | try:
27 | podcasts = await self.podcast_repository.find_by_book_id(book_id)
28 |
29 | logger.debug(f"Found {len(podcasts)} podcasts for book {book_id}")
30 |
31 | return podcasts
32 |
33 | except Exception as e:
34 | logger.error(f"Error finding podcasts for book {book_id}: {str(e)}")
35 | raise
36 |
--------------------------------------------------------------------------------
/apps/api/src/infrastructure/memory/retry_decorator.py:
--------------------------------------------------------------------------------
1 | """再試行デコレータユーティリティ."""
2 |
3 | import logging
4 | import time
5 | from collections.abc import Callable
6 | from functools import wraps
7 | from typing import Any
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def retry_on_error(max_retries: int = 3, initial_delay: int = 1, backoff_factor: int = 2) -> Callable:
13 | """エラー発生時に再試行するデコレータ."""
14 |
15 | def decorator(func: Callable) -> Callable:
16 | @wraps(func)
17 | def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
18 | retries = 0
19 | delay = initial_delay
20 | while True:
21 | try:
22 | return func(*args, **kwargs)
23 | except Exception as e:
24 | retries += 1
25 | if retries > max_retries:
26 | logger.error(f"最大再試行回数 ({max_retries}) に達しました: {str(e)}")
27 | raise
28 | logger.warning(f"操作失敗、{delay}秒後に再試行 ({retries}/{max_retries}): {str(e)}")
29 | time.sleep(delay)
30 | delay *= backoff_factor
31 |
32 | return wrapper
33 |
34 | return decorator
35 |
--------------------------------------------------------------------------------
/apps/reader/src/components/reader/ReaderPaneHeader.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect } from 'react'
2 | import { MdChevronRight } from 'react-icons/md'
3 | import { useSnapshot } from 'valtio'
4 |
5 | import { BookTab } from '../../models'
6 |
7 | import { Bar } from './Bar'
8 |
9 | interface ReaderPaneHeaderProps {
10 | tab: BookTab
11 | }
12 |
13 | export const ReaderPaneHeader: FC = ({ tab }) => {
14 | const { location } = useSnapshot(tab)
15 | const navPath = tab.getNavPath()
16 |
17 | useEffect(() => {
18 | navPath.forEach((i) => (i.expanded = true))
19 | }, [navPath])
20 |
21 | return (
22 |
23 |
24 | {navPath.map((item, i) => (
25 |
29 | {item.label}
30 | {i !== navPath.length - 1 && }
31 |
32 | ))}
33 |
34 | {location && (
35 |
36 | {location.start.displayed.page} / {location.start.displayed.total}
37 |
38 | )}
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/packages/epubjs/types/store.d.ts:
--------------------------------------------------------------------------------
1 | import localForage = require('localforage')
2 |
3 | import Resources from './resources'
4 |
5 | export default class Store {
6 | constructor(name: string, request?: Function, resolver?: Function)
7 |
8 | add(resources: Resources, force?: boolean): Promise>
9 |
10 | put(url: string, withCredentials?: boolean, headers?: object): Promise
11 |
12 | request(
13 | url: string,
14 | type?: string,
15 | withCredentials?: boolean,
16 | headers?: object,
17 | ): Promise
18 |
19 | retrieve(
20 | url: string,
21 | type?: string,
22 | ): Promise
23 |
24 | getBlob(url: string, mimeType?: string): Promise
25 |
26 | getText(url: string): Promise
27 |
28 | getBase64(url: string, mimeType?: string): Promise
29 |
30 | createUrl(url: string, options: { base64: boolean }): Promise
31 |
32 | revokeUrl(url: string): void
33 |
34 | destroy(): void
35 |
36 | private checkRequirements(): void
37 |
38 | private handleResponse(
39 | response: any,
40 | type?: string,
41 | ): Blob | string | JSON | Document | XMLDocument
42 | }
43 |
--------------------------------------------------------------------------------
/packages/epubjs/types/layout.d.ts:
--------------------------------------------------------------------------------
1 | import Contents from './contents'
2 |
3 | interface LayoutSettings {
4 | layout: string
5 | spread: string
6 | minSpreadWidth: number
7 | evenSpreads: boolean
8 | }
9 |
10 | export default class Layout {
11 | constructor(settings: LayoutSettings)
12 |
13 | settings: LayoutSettings
14 | name: string
15 | props: {
16 | name: string
17 | spread: string
18 | flow: string
19 | width: number
20 | height: number
21 | spreadWidth: number
22 | delta: number
23 | columnWidth: number
24 | gap: number
25 | divisor: number
26 | }
27 |
28 | flow(flow: string): string
29 |
30 | spread(spread: string, min: number): boolean
31 |
32 | calculate(_width: number, _height: number, _gap?: number): void
33 |
34 | format(contents: Contents): void | Promise
35 |
36 | count(
37 | totalLength: number,
38 | pageLength: number,
39 | ): { spreads: number; pages: number }
40 |
41 | // Event emitters
42 | emit(type: any, ...args: any[]): void
43 |
44 | off(type: any, listener: any): any
45 |
46 | on(type: any, listener: any): any
47 |
48 | once(type: any, listener: any, ...args: any[]): any
49 |
50 | private update(props: object): void
51 | }
52 |
--------------------------------------------------------------------------------
/packages/epubjs/types/navigation.d.ts:
--------------------------------------------------------------------------------
1 | export interface NavItem {
2 | id: string
3 | href: string
4 | label: string
5 | subitems?: Array
6 | parent?: string
7 | }
8 |
9 | export interface LandmarkItem {
10 | href?: string
11 | label?: string
12 | type?: string
13 | }
14 |
15 | export default class Navigation {
16 | constructor(xml: XMLDocument)
17 |
18 | toc: Array
19 | tocByHref: Record
20 | tocById: Record
21 | landmarks: Array
22 |
23 | parse(xml: XMLDocument): void
24 |
25 | get(target: string): NavItem
26 |
27 | landmark(type: string): LandmarkItem
28 |
29 | load(json: string): Array
30 |
31 | forEach(fn: (item: NavItem) => {}): any
32 |
33 | private unpack(toc: Array): void
34 |
35 | private parseNav(navHtml: XMLDocument): Array
36 |
37 | private navItem(item: Element): NavItem
38 |
39 | private parseLandmarks(navHtml: XMLDocument): Array
40 |
41 | private landmarkItem(item: Element): LandmarkItem
42 |
43 | private parseNcx(navHtml: XMLDocument): Array
44 |
45 | private ncxItem(item: Element): NavItem
46 |
47 | getByIndex(target: string, index: number, navItems: NavItem[]): NavItem
48 | }
49 |
--------------------------------------------------------------------------------
/apps/reader/src/components/reader/ReaderPaneFooter.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx'
2 | import React, { FC } from 'react'
3 | import { useSnapshot } from 'valtio'
4 |
5 | import { BookTab } from '../../models'
6 |
7 | import { Bar } from './Bar'
8 |
9 | interface ReaderPaneFooterProps {
10 | tab: BookTab
11 | }
12 |
13 | export const ReaderPaneFooter: FC = ({ tab }) => {
14 | const { locationToReturn, location, book } = useSnapshot(tab)
15 |
16 | return (
17 |
18 | {locationToReturn ? (
19 | <>
20 | {
23 | tab.hidePrevLocation()
24 | tab.display(locationToReturn.end.cfi, false)
25 | }}
26 | >
27 | Return to {locationToReturn.end.cfi}
28 |
29 | {
31 | tab.hidePrevLocation()
32 | }}
33 | >
34 | Stay
35 |
36 | >
37 | ) : (
38 | <>
39 | {location?.start.href}
40 | {((book?.percentage ?? 0) * 100).toFixed()}%
41 | >
42 | )}
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/message/find_messages_usecase.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.message.entities.message import Message
4 | from src.domain.message.repositories.message_repository import MessageRepository
5 |
6 |
7 | class FindMessagesUseCase(ABC):
8 | @abstractmethod
9 | def execute_find_all(self) -> list[Message]:
10 | """全てのMessageを取得する"""
11 |
12 | @abstractmethod
13 | def execute_find_by_chat_id(self, chat_id: str) -> list[Message]:
14 | """チャットIDでMessageを検索する"""
15 |
16 | @abstractmethod
17 | def execute_find_by_sender_id(self, sender_id: str) -> list[Message]:
18 | """送信者IDでMessageを検索する"""
19 |
20 |
21 | class FindMessagesUseCaseImpl(FindMessagesUseCase):
22 | def __init__(self, message_repository: MessageRepository) -> None:
23 | self.message_repository = message_repository
24 |
25 | def execute_find_all(self) -> list[Message]:
26 | return self.message_repository.find_all()
27 |
28 | def execute_find_by_chat_id(self, chat_id: str) -> list[Message]:
29 | return self.message_repository.find_by_chat_id(chat_id)
30 |
31 | def execute_find_by_sender_id(self, sender_id: str) -> list[Message]:
32 | return self.message_repository.find_by_sender_id(sender_id)
33 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/loading/useProgressManager.ts:
--------------------------------------------------------------------------------
1 | import { useSetAtom } from 'jotai'
2 | import { useCallback } from 'react'
3 |
4 | import { updateTaskAtom, LoadingTask } from '../../store/loading'
5 |
6 | /**
7 | * プログレス管理を担当するフック
8 | */
9 | export function useProgressManager(getCurrentTaskId: () => string | null) {
10 | const updateTask = useSetAtom(updateTaskAtom)
11 |
12 | const updateProgress = useCallback(
13 | (current: number, total: number) => {
14 | const currentTaskId = getCurrentTaskId()
15 | if (!currentTaskId) return
16 |
17 | updateTask({
18 | id: currentTaskId,
19 | updates: {
20 | progress: { current, total },
21 | },
22 | })
23 | },
24 | [updateTask, getCurrentTaskId],
25 | )
26 |
27 | const updateSubTasks = useCallback(
28 | (subTasksUpdate: Partial) => {
29 | const currentTaskId = getCurrentTaskId()
30 | if (!currentTaskId) return
31 |
32 | updateTask({
33 | id: currentTaskId,
34 | updates: {
35 | subTasks: subTasksUpdate as LoadingTask['subTasks'],
36 | },
37 | })
38 | },
39 | [updateTask, getCurrentTaskId],
40 | )
41 |
42 | return {
43 | updateProgress,
44 | updateSubTasks,
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/titlepage.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Alice's Adventures in Wonderland
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Alice's Adventures In Wonderland
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Copyright, 1916,
29 | By Sam'l Gabriel Sons & Company
30 | New York
31 |
32 |
33 |
34 |
35 |
36 | Alice in the Room of the Duchess.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/podcast/podcast_config.py:
--------------------------------------------------------------------------------
1 | """Configuration for podcast use cases"""
2 |
3 | from dataclasses import dataclass
4 |
5 |
6 | @dataclass(frozen=True)
7 | class PodcastConfig:
8 | """Configuration settings for podcast generation"""
9 |
10 | # Chapter extraction settings
11 | max_chapters: int = 15
12 |
13 | # Script generation settings
14 | target_words: int = 1000
15 | min_script_turns: int = 6
16 | max_script_turns: int = 30
17 | max_retries_script_generation: int = 3
18 | initial_temperature: float = 0.7
19 | temperature_increment: float = 0.1
20 | max_text_length_per_turn: int = 500
21 |
22 | # Summarization settings
23 | max_concurrent_summarization_requests: int = 2
24 | chapter_summary_chunk_size: int = 5
25 | chapter_content_clip_lengths: tuple[int, ...] = (6000, 4000, 2000)
26 | chapter_summary_max_tokens: tuple[int, ...] = (400, 350, 300)
27 | partial_summary_max_tokens: int = 800
28 | final_summary_max_tokens: int = 1200
29 | summarization_temperature: float = 0.3
30 |
31 | # Audio synthesis settings
32 | max_chars_per_tts_request: int = 5000
33 | audio_synthesis_batch_size: int = 10
34 |
35 | # General settings
36 | min_speaker_participation: float = 0.2 # Each speaker should have at least 20% of turns
37 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from 'class-variance-authority'
2 | import * as React from 'react'
3 |
4 | import { cn } from '@flow/reader/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | },
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/apps/reader/src/constants/audio.ts:
--------------------------------------------------------------------------------
1 | interface SpeedOption {
2 | value: number
3 | label: string
4 | }
5 |
6 | // オーディオ制御関連の定数
7 | export const AUDIO_CONTROLS = {
8 | SKIP_SECONDS: 10, // スキップする秒数
9 | VOLUME_STEP: 0.1, // 音量調整のステップ
10 | DEFAULT_VOLUME: 1.0, // デフォルト音量
11 | MIN_VOLUME: 0, // 最小音量
12 | MAX_VOLUME: 1, // 最大音量
13 | } as const
14 |
15 | // 長押し関連の定数
16 | export const LONG_PRESS = {
17 | DEFAULT_INTERVAL: 200, // 長押し時の間隔(ミリ秒)
18 | INITIAL_DELAY: 500, // 長押し判定の遅延(ミリ秒)
19 | } as const
20 |
21 | // 再生速度オプション
22 | export const SPEED_OPTIONS: SpeedOption[] = [
23 | { value: 0.5, label: '0.5x' },
24 | { value: 0.75, label: '0.75x' },
25 | { value: 1, label: '1x' },
26 | { value: 1.25, label: '1.25x' },
27 | { value: 1.5, label: '1.5x' },
28 | { value: 2, label: '2x' },
29 | ]
30 |
31 | // オーディオ関連のキー定数
32 | export const AUDIO_EVENTS = {
33 | TIME_UPDATE: 'timeupdate',
34 | LOADED_METADATA: 'loadedmetadata',
35 | ENDED: 'ended',
36 | LOAD_START: 'loadstart',
37 | CAN_PLAY: 'canplay',
38 | ERROR: 'error',
39 | } as const
40 |
41 | // 音量スライダー関連の定数
42 | export const VOLUME_SLIDER = {
43 | MIN: 0,
44 | MAX: 1,
45 | STEP: 0.01,
46 | } as const
47 |
48 | // オーディオファイル関連の定数
49 | export const AUDIO_FILE = {
50 | PRELOAD: 'metadata' as const,
51 | CROSSORIGIN: 'anonymous' as const,
52 | }
53 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/podcast/find_podcast_by_id_usecase.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from src.domain.podcast.entities.podcast import Podcast
4 | from src.domain.podcast.repositories.podcast_repository import PodcastRepository
5 | from src.domain.podcast.value_objects.podcast_id import PodcastId
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | class FindPodcastByIdUseCase:
11 | """Use case for finding a podcast by its ID"""
12 |
13 | def __init__(self, podcast_repository: PodcastRepository) -> None:
14 | self.podcast_repository = podcast_repository
15 |
16 | async def execute(self, podcast_id: PodcastId) -> Podcast | None:
17 | """Find a podcast by its ID
18 |
19 | Args:
20 | podcast_id: ID of the podcast to find
21 |
22 | Returns:
23 | Podcast entity if found, None otherwise
24 |
25 | """
26 | try:
27 | podcast = await self.podcast_repository.find_by_id(podcast_id)
28 |
29 | if podcast:
30 | logger.debug(f"Found podcast {podcast_id}")
31 | else:
32 | logger.debug(f"Podcast {podcast_id} not found")
33 |
34 | return podcast
35 |
36 | except Exception as e:
37 | logger.error(f"Error finding podcast {podcast_id}: {str(e)}")
38 | raise
39 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // ========================= general =========================
3 | "files.insertFinalNewline": true,
4 | "editor.formatOnSave": true,
5 |
6 | // ========================= typescript =========================
7 | "typescript.tsdk": "./node_modules/typescript/lib",
8 | // https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-1979394
9 | "css.lint.unknownAtRules": "ignore",
10 | "editor.codeActionsOnSave": {
11 | "source.fixAll.eslint": "always"
12 | },
13 |
14 | // ========================= python =========================
15 | "[python]": {
16 | "editor.codeActionsOnSave": {
17 | "source.organizeImports.ruff": "explicit",
18 | "source.fixAll.ruff": "explicit"
19 | },
20 | "editor.defaultFormatter": "charliermarsh.ruff"
21 | },
22 | "python.defaultInterpreterPath": "${workspaceFolder}/apps/api/.venv/bin/python",
23 | "ruff.path": ["${workspaceFolder}/apps/api/.venv/bin/ruff"],
24 | "cSpell.words": ["ebooklib", "unsummarized"],
25 | "mypy-type-checker.path": ["${workspaceFolder}/apps/api/.venv/bin/mypy"],
26 | "mypy-type-checker.args": [
27 | "--config-file=${workspaceFolder}/apps/api/pyproject.toml"
28 | ],
29 | "python.analysis.autoImportCompletions": true,
30 | "python.analysis.extraPaths": ["${workspaceFolder}/apps/api"]
31 | }
32 |
--------------------------------------------------------------------------------
/apps/reader/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
2 | import * as React from 'react'
3 |
4 | import { cn } from '@flow/reader/lib/utils'
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
17 |
26 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/apps/reader/src/hooks/useSWR/useChat.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 |
3 | import { components } from '../../lib/openapi-schema/schema'
4 |
5 | import { fetcher } from './fetcher'
6 |
7 | /**
8 | * ユーザーIDに紐づくチャット一覧を取得するSWRフック
9 | * @param userId ユーザーID (null の場合は fetch しない)
10 | */
11 | export const useGetUserChats = (userId: string | null) => {
12 | const { data, error, isValidating, mutate } = useSWR<
13 | components['schemas']['ChatsResponse']
14 | >(
15 | // userId が null でない場合のみ fetch する
16 | userId
17 | ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/chats/user/${userId}`
18 | : null,
19 | fetcher,
20 | )
21 |
22 | return {
23 | chats: data,
24 | error,
25 | isLoading: isValidating,
26 | mutateChats: mutate,
27 | }
28 | }
29 |
30 | /**
31 | * チャットIDに紐づくメッセージ一覧を取得するSWRフック
32 | * @param chatId チャットID (null の場合は fetch しない)
33 | */
34 | export const useGetChatMessages = (chatId: string | null) => {
35 | const { data, error, isValidating, mutate } = useSWR<
36 | components['schemas']['MessageListResponse']
37 | >(
38 | chatId
39 | ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/messages/${chatId}`
40 | : null,
41 | fetcher,
42 | )
43 |
44 | return {
45 | messages: data?.messages || [],
46 | error,
47 | isLoading: isValidating,
48 | mutateMessages: mutate,
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/epubjs/src/container.js:
--------------------------------------------------------------------------------
1 | import path from 'path-webpack'
2 |
3 | import { qs } from './utils/core'
4 |
5 | /**
6 | * Handles Parsing and Accessing an Epub Container
7 | * @class
8 | * @param {document} [containerDocument] xml document
9 | */
10 | class Container {
11 | constructor(containerDocument) {
12 | this.packagePath = ''
13 | this.directory = ''
14 | this.encoding = ''
15 |
16 | if (containerDocument) {
17 | this.parse(containerDocument)
18 | }
19 | }
20 |
21 | /**
22 | * Parse the Container XML
23 | * @param {document} containerDocument
24 | */
25 | parse(containerDocument) {
26 | //--
27 | var rootfile
28 |
29 | if (!containerDocument) {
30 | throw new Error('Container File Not Found')
31 | }
32 |
33 | rootfile = qs(containerDocument, 'rootfile')
34 |
35 | if (!rootfile) {
36 | throw new Error('No RootFile Found')
37 | }
38 |
39 | this.packagePath = rootfile.getAttribute('full-path')
40 | this.directory = path.dirname(this.packagePath)
41 | this.encoding = containerDocument.xmlEncoding
42 | }
43 |
44 | destroy() {
45 | this.packagePath = undefined
46 | this.directory = undefined
47 | this.encoding = undefined
48 | }
49 | }
50 |
51 | export default Container
52 |
--------------------------------------------------------------------------------
/apps/api/src/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from collections.abc import Generator
3 | from contextlib import asynccontextmanager
4 |
5 | from fastapi import FastAPI
6 | from fastapi.middleware.cors import CORSMiddleware
7 | from sqlalchemy.orm import Session
8 |
9 | from src.config.db import get_db, init_db
10 | from src.presentation.api import setup_routes
11 | from src.presentation.api.error_messages.error_handlers import setup_exception_handlers
12 |
13 | logging.basicConfig(
14 | level=logging.INFO,
15 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
16 | )
17 |
18 |
19 | @asynccontextmanager
20 | async def lifespan(app: FastAPI):
21 | # Startup
22 | try:
23 | init_db()
24 | logging.info("Database connection established")
25 | except Exception as e:
26 | logging.error(f"Database initialization error: {e}")
27 |
28 | yield
29 |
30 | # Shutdown
31 | logging.info("Closing database connection")
32 |
33 |
34 | app = FastAPI(title="BookWith API", description="Book related API service", lifespan=lifespan)
35 |
36 | app.add_middleware(
37 | CORSMiddleware,
38 | allow_origins=["*"],
39 | allow_credentials=True,
40 | allow_methods=["*"],
41 | allow_headers=["*"],
42 | )
43 |
44 |
45 | def get_db_session() -> Generator[Session]:
46 | yield from get_db()
47 |
48 |
49 | setup_routes(app)
50 |
51 | setup_exception_handlers(app)
52 |
--------------------------------------------------------------------------------
/apps/reader/src/constants/podcast.ts:
--------------------------------------------------------------------------------
1 | // エラーメッセージのキー
2 | export const PODCAST_ERROR_KEYS = {
3 | GENERATION_FAILED: 'podcast.pane.generation_failed',
4 | REGENERATION_FAILED: 'podcast.pane.regeneration_failed',
5 | LOADING_FAILED: 'podcast.pane.loading_failed',
6 | PLAYBACK_FAILED: 'podcast.audio_player.playback_failed',
7 | NETWORK_ERROR: 'podcast.errors.network_error',
8 | UNKNOWN_ERROR: 'podcast.errors.unknown',
9 | } as const
10 |
11 | // ポッドキャストのアイコンサイズ
12 | export const PODCAST_ICON_SIZES = {
13 | XS: 'h-3 w-3',
14 | SM: 'h-4 w-4',
15 | MD: 'h-6 w-6',
16 | LG: 'h-8 w-8',
17 | } as const
18 |
19 | // キーボードショートカット
20 | export const PODCAST_KEYBOARD_SHORTCUTS = {
21 | PLAY_PAUSE: ' ',
22 | SKIP_FORWARD: 'ArrowRight',
23 | SKIP_BACK: 'ArrowLeft',
24 | VOLUME_UP: 'ArrowUp',
25 | VOLUME_DOWN: 'ArrowDown',
26 | SPEED_UP: '>',
27 | SPEED_DOWN: '<',
28 | MUTE_TOGGLE: 'm',
29 | } as const
30 |
31 | // UIコンポーネントのクラス名
32 | export const PODCAST_UI_CLASSES = {
33 | ERROR_CARD: 'rounded-md border border-red-200 bg-red-50 p-3',
34 | PROCESSING_CARD: 'border-blue-200 bg-blue-50 p-6',
35 | FAILED_CARD: 'border-destructive/50 bg-destructive/5 p-6',
36 | CONTROL_BUTTON: 'h-8 w-8',
37 | PLAY_BUTTON: 'h-12 w-12',
38 | } as const
39 |
40 | // アニメーションクラス
41 | export const PODCAST_ANIMATIONS = {
42 | SPIN: 'animate-spin',
43 | FADE_IN: 'animate-fade-in',
44 | FADE_OUT: 'animate-fade-out',
45 | } as const
46 |
--------------------------------------------------------------------------------
/packages/epubjs/types/section.d.ts:
--------------------------------------------------------------------------------
1 | import { HooksObject } from './utils/hook'
2 |
3 | export interface GlobalLayout {
4 | layout: string
5 | spread: string
6 | orientation: string
7 | }
8 |
9 | export interface LayoutSettings {
10 | layout: string
11 | spread: string
12 | orientation: string
13 | }
14 |
15 | export interface SpineItem {
16 | index: number
17 | cfiBase: string
18 | href?: string
19 | url?: string
20 | canonical?: string
21 | properties?: Array
22 | linear?: string
23 | next: () => SpineItem
24 | prev: () => SpineItem
25 | }
26 |
27 | export default class Section {
28 | constructor(item: SpineItem, hooks: HooksObject)
29 |
30 | idref: string
31 | linear: boolean
32 | properties: Array
33 | index: number
34 | href: string
35 | url: string
36 | canonical: string
37 | next: () => SpineItem
38 | prev: () => SpineItem
39 | cfiBase: string
40 |
41 | document: Document
42 | contents: Element
43 | output: string
44 |
45 | hooks: HooksObject
46 |
47 | load(_request?: Function): Document
48 |
49 | render(_request?: Function): string
50 |
51 | find(_query: string): Array
52 |
53 | reconcileLayoutSettings(globalLayout: GlobalLayout): LayoutSettings
54 |
55 | cfiFromRange(_range: Range): string
56 |
57 | cfiFromElement(el: Element): string
58 |
59 | unload(): void
60 |
61 | destroy(): void
62 |
63 | private base(): void
64 | }
65 |
--------------------------------------------------------------------------------
/apps/reader/src/store/loading/selectors.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai'
2 |
3 | import { LoadingTask } from '../../types/loading'
4 |
5 | import { loadingTasksAtom } from './atoms'
6 |
7 | /**
8 | * Derived atom that checks if any global loading task exists
9 | */
10 | export const isGlobalLoadingAtom = atom((get) => {
11 | const tasks = get(loadingTasksAtom)
12 | return Array.from(tasks.values()).some((task) => task.type === 'global')
13 | })
14 |
15 | /**
16 | * Derived atom that returns the count of active tasks
17 | */
18 | export const activeTasksCountAtom = atom((get) => get(loadingTasksAtom).size)
19 |
20 | /**
21 | * Derived atom that returns the primary (first global) task
22 | */
23 | export const primaryTaskAtom = atom((get) => {
24 | const tasks = get(loadingTasksAtom)
25 | const globalTasks = Array.from(tasks.values()).filter(
26 | (task) => task.type === 'global',
27 | )
28 | return globalTasks[0] || null
29 | })
30 |
31 | /**
32 | * Derived atom that returns all global tasks
33 | */
34 | export const globalTasksAtom = atom((get) => {
35 | const tasks = get(loadingTasksAtom)
36 | return Array.from(tasks.values()).filter((task) => task.type === 'global')
37 | })
38 |
39 | /**
40 | * Derived atom that returns all local tasks
41 | */
42 | export const localTasksAtom = atom((get) => {
43 | const tasks = get(loadingTasksAtom)
44 | return Array.from(tasks.values()).filter((task) => task.type === 'local')
45 | })
46 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/book/__init__.py:
--------------------------------------------------------------------------------
1 | from src.usecase.book.create_book_usecase import (
2 | CreateBookUseCase,
3 | CreateBookUseCaseImpl,
4 | )
5 | from src.usecase.book.create_book_vector_index_usecase import (
6 | CreateBookVectorIndexUseCase,
7 | CreateBookVectorIndexUseCaseImpl,
8 | )
9 | from src.usecase.book.delete_book_usecase import (
10 | BulkDeleteBooksUseCase,
11 | BulkDeleteBooksUseCaseImpl,
12 | DeleteBookUseCase,
13 | DeleteBookUseCaseImpl,
14 | )
15 | from src.usecase.book.find_book_by_id_usecase import (
16 | FindBookByIdUseCase,
17 | FindBookByIdUseCaseImpl,
18 | )
19 | from src.usecase.book.find_books_usecase import (
20 | FindBooksByUserIdUseCase,
21 | FindBooksByUserIdUseCaseImpl,
22 | FindBooksUseCase,
23 | FindBooksUseCaseImpl,
24 | )
25 | from src.usecase.book.update_book_usecase import (
26 | UpdateBookUseCase,
27 | UpdateBookUseCaseImpl,
28 | )
29 |
30 | __all__ = [
31 | "CreateBookUseCase",
32 | "CreateBookUseCaseImpl",
33 | "CreateBookVectorIndexUseCase",
34 | "CreateBookVectorIndexUseCaseImpl",
35 | "BulkDeleteBooksUseCase",
36 | "BulkDeleteBooksUseCaseImpl",
37 | "DeleteBookUseCase",
38 | "DeleteBookUseCaseImpl",
39 | "FindBookByIdUseCase",
40 | "FindBookByIdUseCaseImpl",
41 | "FindBooksByUserIdUseCase",
42 | "FindBooksByUserIdUseCaseImpl",
43 | "FindBooksUseCase",
44 | "FindBooksUseCaseImpl",
45 | "UpdateBookUseCase",
46 | "UpdateBookUseCaseImpl",
47 | ]
48 |
--------------------------------------------------------------------------------
/packages/epubjs/examples/toc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | EPUB.js Basic Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/apps/api/src/usecase/message/highlight_searcher.py:
--------------------------------------------------------------------------------
1 | """ハイライト検索サービス."""
2 |
3 | import logging
4 |
5 | from src.infrastructure.memory.memory_vector_store import MemoryVectorStore
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | class HighlightSearcher:
11 | """書籍のハイライト(アノテーション)検索を行うサービス."""
12 |
13 | def __init__(self) -> None:
14 | """ハイライト検索サービスの初期化."""
15 | self.memory_vector_store = MemoryVectorStore()
16 |
17 | def search_relevant_highlights(self, question: str, user_id: str, book_id: str, limit: int = 3) -> list[str]:
18 | """質問に関連するハイライトテキストを検索する."""
19 | if not (user_id and book_id):
20 | return ["No highlights found"]
21 |
22 | # 質問をベクトル化
23 | query_vector = self.memory_vector_store.encode_text(question)
24 |
25 | # ハイライトを検索
26 | try:
27 | highlights = self.memory_vector_store.search_highlights(user_id=user_id, book_id=book_id, query_vector=query_vector, limit=limit)
28 | except Exception as e:
29 | logger.error(f"Failed to search highlights: {e}")
30 | return ["検索でエラーが発生しました"]
31 |
32 | if not highlights:
33 | return ["No highlights found"]
34 |
35 | # ハイライトテキストを整形
36 | highlight_texts = []
37 | for h in highlights:
38 | highlight_text = h["content"]
39 | if h.get("notes"):
40 | highlight_text += f"\n{h['notes']}"
41 | highlight_texts.append(f"【ハイライト】{highlight_text}")
42 |
43 | return highlight_texts
44 |
--------------------------------------------------------------------------------
/apps/api/src/domain/book/value_objects/book_metadata.py:
--------------------------------------------------------------------------------
1 | from dataclasses import asdict, dataclass
2 | from typing import Any
3 |
4 |
5 | @dataclass(frozen=True)
6 | class BookMetadata:
7 | """EPub書籍のメタデータ値オブジェクト"""
8 |
9 | title: str | None = None
10 | creator: str | None = None
11 | description: str | None = None
12 | pubdate: str | None = None
13 | publisher: str | None = None
14 | identifier: str | None = None
15 | language: str | None = None
16 | rights: str | None = None
17 | modified_date: str | None = None
18 | layout: str | None = None
19 | orientation: str | None = None
20 | flow: str | None = None
21 | viewport: str | None = None
22 | spread: str | None = None
23 |
24 | def to_dict(self) -> dict[str, Any]:
25 | """辞書形式に変換(None値は除外)"""
26 | return {k: v for k, v in asdict(self).items() if v is not None}
27 |
28 | @classmethod
29 | def from_dict(cls, data: dict[str, Any]) -> "BookMetadata":
30 | """辞書から生成"""
31 | # 辞書に存在するフィールドのみを渡し、デフォルト値を正しく使用させる
32 | return cls(**{field: data[field] for field in cls.__dataclass_fields__ if field in data})
33 |
34 | @classmethod
35 | def from_json_string(cls, json_string: str | None) -> "BookMetadata":
36 | """JSON文字列から生成"""
37 | if not json_string:
38 | return cls()
39 |
40 | import json
41 |
42 | try:
43 | data = json.loads(json_string)
44 | return cls.from_dict(data)
45 | except json.JSONDecodeError:
46 | return cls()
47 |
--------------------------------------------------------------------------------
/apps/api/src/presentation/api/handlers/rag_api_route_handler.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from io import BytesIO
3 |
4 | from fastapi import APIRouter, Depends, UploadFile
5 | from starlette.datastructures import Headers
6 |
7 | from src.infrastructure.di.injection import get_create_book_vector_index_usecase
8 | from src.presentation.api.error_messages.error_handlers import (
9 | BadRequestException,
10 | ServiceUnavailableException,
11 | )
12 | from src.presentation.api.schemas.book_schema import RagProcessRequest, RagProcessResponse
13 | from src.usecase.book.create_book_vector_index_usecase import CreateBookVectorIndexUseCase
14 |
15 | router = APIRouter()
16 |
17 |
18 | @router.post("", response_model=RagProcessResponse)
19 | async def upload_and_process_rag(
20 | request: RagProcessRequest,
21 | usecase: CreateBookVectorIndexUseCase = Depends(get_create_book_vector_index_usecase),
22 | ):
23 | """Base64 で送られてきた EPUB をデコードし、ベクトルストアにインデックス化する."""
24 | try:
25 | decoded_bytes = base64.b64decode(request.file_data)
26 | file_like = BytesIO(decoded_bytes)
27 | upload_file = UploadFile(
28 | file_like,
29 | filename=request.file_name,
30 | headers=Headers({"content-type": "application/epub+zip"}),
31 | )
32 |
33 | return await usecase.execute(upload_file, request.user_id, request.book_id)
34 | except ValueError as e:
35 | raise BadRequestException(str(e))
36 | except Exception as e:
37 | raise ServiceUnavailableException(f"Error occurred while processing file: {str(e)}")
38 |
--------------------------------------------------------------------------------
/apps/api/src/domain/message/repositories/message_repository.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from src.domain.message.entities.message import Message
4 | from src.domain.message.value_objects.message_id import MessageId
5 |
6 |
7 | class MessageRepository(ABC):
8 | @abstractmethod
9 | def save(self, message: Message) -> None:
10 | """メッセージを保存する."""
11 |
12 | @abstractmethod
13 | def find_by_id(self, message_id: MessageId) -> Message | None:
14 | """IDでメッセージを検索する."""
15 |
16 | @abstractmethod
17 | def find_all(self) -> list[Message]:
18 | """全てのメッセージを取得する."""
19 |
20 | @abstractmethod
21 | def find_by_chat_id(self, chat_id: str) -> list[Message]:
22 | """チャットIDでメッセージを検索する."""
23 |
24 | @abstractmethod
25 | def find_latest_by_chat_id(self, chat_id: str, limit: int) -> list[Message]:
26 | """チャットIDで最新のメッセージを指定件数取得する."""
27 |
28 | @abstractmethod
29 | def find_by_sender_id(self, sender_id: str) -> list[Message]:
30 | """送信者IDでメッセージを検索する."""
31 |
32 | @abstractmethod
33 | def delete(self, message_id: MessageId) -> None:
34 | """IDでメッセージを削除する."""
35 |
36 | @abstractmethod
37 | def bulk_delete(self, message_ids: list[MessageId]) -> list[MessageId]:
38 | """複数のメッセージを一括削除する。削除に失敗したメッセージIDのリストを返す."""
39 |
40 | @abstractmethod
41 | def count_by_chat_id(self, chat_id: str) -> int:
42 | """チャットIDに関連するメッセージの数を取得する."""
43 |
44 | @abstractmethod
45 | def find_chat_ids_by_user_id(self, user_id: str) -> list[str]:
46 | """ユーザーIDに関連するチャットIDを取得する."""
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@flow/monorepo",
3 | "private": true,
4 | "type": "module",
5 | "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0",
6 | "scripts": {
7 | "build": "turbo run build",
8 | "dev": "turbo run dev --parallel",
9 | "lint": "turbo run lint",
10 | "release": "pnpm -r publish --access public",
11 | "prepare": "husky install"
12 | },
13 | "lint-staged": {
14 | "*.{js,json,css,ts,tsx,md,mdx}": "prettier --write",
15 | "!(pnpm-lock).{yml,yaml}": "prettier --write",
16 | "*.{js,ts,tsx}": "eslint --fix"
17 | },
18 | "devDependencies": {
19 | "@changesets/changelog-github": "0.4.4",
20 | "@changesets/cli": "2.22.0",
21 | "@typescript-eslint/eslint-plugin": "5.19.0",
22 | "cross-env": "7.0.3",
23 | "eslint": "9.25.1",
24 | "eslint-config-next": "15.0.0",
25 | "eslint-config-prettier": "8.5.0",
26 | "eslint-plugin-react": "7.29.4",
27 | "esno": "0.14.1",
28 | "globals": "^16.0.0",
29 | "husky": "7.0.4",
30 | "lint-staged": "12.3.7",
31 | "prettier": "2.6.2",
32 | "prettier-plugin-tailwindcss": "0.1.8",
33 | "rimraf": "3.0.2",
34 | "rollup": "2.72.1",
35 | "rollup-plugin-dts": "4.2.1",
36 | "rollup-plugin-typescript2": "0.31.2",
37 | "tsup": "5.12.7",
38 | "turbo": "2.5.0",
39 | "typescript": "5.8.3"
40 | },
41 | "engines": {
42 | "node": ">=18.0.0"
43 | },
44 | "pnpm": {
45 | "overrides": {
46 | "@types/react": "19.1.0"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/epubjs/test/fixtures/alice/OPS/toc.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Alice's Adventures in Wonderland
6 |
7 |
8 |
9 |
10 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/packages/epubjs/license:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, FuturePress
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
25 | The views and conclusions contained in the software and documentation are those
26 | of the authors and should not be interpreted as representing official policies,
27 | either expressed or implied, of the FreeBSD Project.
28 |
--------------------------------------------------------------------------------