((ref) {
112 | return LocaleNotifier();
113 | });
114 |
--------------------------------------------------------------------------------
/README_KO.md:
--------------------------------------------------------------------------------
1 | # 📚 Olib
2 |
3 |
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 | **🤖 AI 지원으로 완전히 구축된 오픈 소스 전자책 리더**
12 |
13 | **서드파티 클라이언트 • 프론트엔드 인터페이스만 제공 • 외부 소스에서 데이터**
14 |
15 | [APK 다운로드](https://bookbook.space) • [버그 신고](../../issues) • [기능 요청](../../issues)
16 |
17 | **[English](README.md)** | **[简体中文](README_ZH.md)** | **[日本語](README_JA.md)** | **한국어**
18 |
19 |
20 |
21 | ---
22 |
23 | > ⚠️ **면책 조항**: Olib은 독립적인 오픈 소스 서드파티 클라이언트입니다. 공식 클라이언트가 **아니며** 어떤 공식 서비스와도 관련이 없습니다. 이 프로젝트는 프론트엔드 인터페이스만 제공하며 모든 책 데이터는 외부 소스에서 가져옵니다. 본인의 판단에 따라 사용하세요.
24 |
25 | ## ✨ 기능
26 |
27 | | 기능 | 설명 |
28 | |------|------|
29 | | 📖 **책 검색** | 제목, 저자, ISBN 또는 키워드로 책 검색 |
30 | | 💾 **오프라인 읽기** | 인터넷 없이 읽기 위해 책 다운로드 |
31 | | 🌙 **다크 모드** | 눈에 편안한 읽기 경험 |
32 | | 🌍 **다국어** | 영어, 中文, 日本語, 한국어 등 16개 이상의 언어 지원 |
33 | | 🔐 **멀티 계정** | 여러 계정을 원활하게 전환 |
34 | | 🔗 **멀티 도메인** | 여러 서버 라인 중 선택 |
35 | | 🆓 **완전 무료** | 광고 없음, 구독 없음, 숨겨진 비용 없음 |
36 |
37 | ## 🤖 AI 구축 프로젝트
38 |
39 | 이 프로젝트는 **완전히 AI 지원으로 구축**되었습니다:
40 | - AI에 의한 아키텍처 설계
41 | - AI에 의한 코드 구현
42 | - AI에 의한 UI/UX 디자인
43 | - AI에 의한 문서 작성
44 |
45 | ## 📱 스크린샷
46 |
47 |
48 | 곧 출시 예정...
49 |
50 |
51 | ## 🚀 빠른 시작
52 |
53 | ### 전제 조건
54 |
55 | - Flutter SDK 3.8+
56 | - Android Studio / VS Code
57 | - Android 기기 또는 에뮬레이터
58 |
59 | ### 설치
60 |
61 | ```bash
62 | # 저장소 복제
63 | git clone https://github.com/shiyi-0x7f/olib-mobile.git
64 |
65 | # 프로젝트 디렉토리로 이동
66 | cd olib-mobile
67 |
68 | # 종속성 설치
69 | flutter pub get
70 |
71 | # 앱 실행
72 | flutter run
73 | ```
74 |
75 | ### APK 빌드
76 |
77 | ```bash
78 | flutter build apk --release
79 | ```
80 |
81 | ## 🏗️ 프로젝트 구조
82 |
83 | ```
84 | lib/
85 | ├── l10n/ # 로컬라이제이션 파일 (16개 이상 언어)
86 | ├── models/ # 데이터 모델
87 | ├── providers/ # 상태 관리 (Riverpod)
88 | ├── routes/ # 앱 네비게이션
89 | ├── screens/ # UI 화면
90 | ├── services/ # API & 스토리지 서비스
91 | ├── theme/ # 앱 테마 설정
92 | └── widgets/ # 재사용 가능한 컴포넌트
93 | ```
94 |
95 | ## 🛠️ 기술 스택
96 |
97 | - **프레임워크**: Flutter
98 | - **상태 관리**: Riverpod
99 | - **로컬 스토리지**: Hive
100 | - **HTTP 클라이언트**: http 패키지
101 | - **다국어**: 16개 이상 언어
102 |
103 | ## 🤝 기여
104 |
105 | 기여를 환영합니다! 자유롭게 Pull Request를 제출해 주세요.
106 |
107 | 1. 프로젝트 포크
108 | 2. 기능 브랜치 생성 (`git checkout -b feature/AmazingFeature`)
109 | 3. 변경 사항 커밋 (`git commit -m 'Add some AmazingFeature'`)
110 | 4. 브랜치에 푸시 (`git push origin feature/AmazingFeature`)
111 | 5. Pull Request 열기
112 |
113 | ## 📄 라이선스
114 |
115 | 이 프로젝트는 MIT 라이선스 하에 라이선스가 부여됩니다 - 자세한 내용은 [LICENSE](LICENSE) 파일을 참조하세요.
116 |
117 | > ⚠️ **법적 고지**:
118 | > - 이것은 독립적인 서드파티 클라이언트이며 공식 애플리케이션이 **아닙니다**
119 | > - 모든 책 데이터는 외부 소스에서 가져오며 이 프로젝트는 프론트엔드만 제공합니다
120 | > - 사용자는 해당 법률을 준수해야 할 책임이 있습니다
121 | > - 이 소프트웨어를 사용함으로써 이러한 조건을 인정하게 됩니다
122 |
123 | ## 💖 감사의 말
124 |
125 | - 🤖 AI 지원으로 구축
126 | - 💙 Flutter 프레임워크
127 | - ❤️ 오픈 소스 커뮤니티
128 |
129 | ---
130 |
131 |
132 |
133 | **[⬆ 맨 위로](#-olib)**
134 |
135 | 🤖 AI 구축 • 오픈 소스 • 영원히 무료
136 |
137 |
138 |
--------------------------------------------------------------------------------
/README_JA.md:
--------------------------------------------------------------------------------
1 | # 📚 Olib
2 |
3 |
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 | **🤖 AIアシスタンスで完全に構築されたオープンソース電子書籍リーダー**
12 |
13 | **サードパーティクライアント • フロントエンドインターフェースのみ • 外部ソースからのデータ**
14 |
15 | [APKをダウンロード](https://bookbook.space) • [バグを報告](../../issues) • [機能をリクエスト](../../issues)
16 |
17 | **[English](README.md)** | **[简体中文](README_ZH.md)** | **日本語** | **[한국어](README_KO.md)**
18 |
19 |
20 |
21 | ---
22 |
23 | > ⚠️ **免責事項**: Olibは独立したオープンソースのサードパーティクライアントです。公式クライアントではなく、いかなる公式サービスとも関連していません。このプロジェクトはフロントエンドインターフェースのみを提供し、すべての書籍データは外部ソースから取得されます。ご自身の判断でお使いください。
24 |
25 | ## ✨ 機能
26 |
27 | | 機能 | 説明 |
28 | |------|------|
29 | | 📖 **書籍検索** | タイトル、著者、ISBN、キーワードで書籍を検索 |
30 | | 💾 **オフライン読書** | インターネットなしで読書するために書籍をダウンロード |
31 | | 🌙 **ダークモード** | 目に優しい読書体験 |
32 | | 🌍 **多言語対応** | 英語、中文、日本語、한국어など16以上の言語をサポート |
33 | | 🔐 **マルチアカウント** | 複数のアカウントをシームレスに切り替え |
34 | | 🔗 **マルチドメイン** | 複数のサーバーラインから選択 |
35 | | 🆓 **完全無料** | 広告なし、サブスクリプションなし、隠れたコストなし |
36 |
37 | ## 🤖 AI構築プロジェクト
38 |
39 | このプロジェクトは**完全にAIアシスタンスで構築**されました:
40 | - AIによるアーキテクチャ設計
41 | - AIによるコード実装
42 | - AIによるUI/UXデザイン
43 | - AIによるドキュメント作成
44 |
45 | ## 📱 スクリーンショット
46 |
47 |
48 | 近日公開予定...
49 |
50 |
51 | ## 🚀 クイックスタート
52 |
53 | ### 前提条件
54 |
55 | - Flutter SDK 3.8+
56 | - Android Studio / VS Code
57 | - Androidデバイスまたはエミュレータ
58 |
59 | ### インストール
60 |
61 | ```bash
62 | # リポジトリをクローン
63 | git clone https://github.com/shiyi-0x7f/olib-mobile.git
64 |
65 | # プロジェクトディレクトリに移動
66 | cd olib-mobile
67 |
68 | # 依存関係をインストール
69 | flutter pub get
70 |
71 | # アプリを実行
72 | flutter run
73 | ```
74 |
75 | ### APKをビルド
76 |
77 | ```bash
78 | flutter build apk --release
79 | ```
80 |
81 | ## 🏗️ プロジェクト構成
82 |
83 | ```
84 | lib/
85 | ├── l10n/ # ローカライゼーションファイル (16以上の言語)
86 | ├── models/ # データモデル
87 | ├── providers/ # 状態管理 (Riverpod)
88 | ├── routes/ # アプリナビゲーション
89 | ├── screens/ # UI画面
90 | ├── services/ # API & ストレージサービス
91 | ├── theme/ # アプリテーマ設定
92 | └── widgets/ # 再利用可能なコンポーネント
93 | ```
94 |
95 | ## 🛠️ 技術スタック
96 |
97 | - **フレームワーク**: Flutter
98 | - **状態管理**: Riverpod
99 | - **ローカルストレージ**: Hive
100 | - **HTTPクライアント**: http パッケージ
101 | - **多言語**: 16以上の言語
102 |
103 | ## 🤝 コントリビューション
104 |
105 | コントリビューションを歓迎します!お気軽にPull Requestを提出してください。
106 |
107 | 1. プロジェクトをフォーク
108 | 2. 機能ブランチを作成 (`git checkout -b feature/AmazingFeature`)
109 | 3. 変更をコミット (`git commit -m 'Add some AmazingFeature'`)
110 | 4. ブランチにプッシュ (`git push origin feature/AmazingFeature`)
111 | 5. Pull Requestを開く
112 |
113 | ## 📄 ライセンス
114 |
115 | このプロジェクトはMITライセンスの下でライセンスされています - 詳細は[LICENSE](LICENSE)ファイルをご覧ください。
116 |
117 | > ⚠️ **法的通知**:
118 | > - これは独立したサードパーティクライアントであり、公式アプリケーションではありません
119 | > - すべての書籍データは外部ソースから取得され、このプロジェクトはフロントエンドのみを提供します
120 | > - ユーザーは適用法への準拠を確認する責任があります
121 | > - このソフトウェアを使用することで、これらの条件を認めたことになります
122 |
123 | ## 💖 謝辞
124 |
125 | - 🤖 AIアシスタンスで構築
126 | - 💙 Flutterフレームワーク
127 | - ❤️ オープンソースコミュニティ
128 |
129 | ---
130 |
131 |
132 |
133 | **[⬆ トップに戻る](#-olib)**
134 |
135 | 🤖 AI構築 • オープンソース • 永久に無料
136 |
137 |
138 |
--------------------------------------------------------------------------------
/lib/screens/settings/history_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import '../../providers/books_provider.dart';
4 | import '../../widgets/gradient_app_bar.dart';
5 | import '../../widgets/loading_widget.dart';
6 | import '../../widgets/empty_state.dart';
7 | import '../../routes/app_routes.dart';
8 | import '../../l10n/app_localizations.dart';
9 |
10 | class HistoryScreen extends ConsumerWidget {
11 | const HistoryScreen({super.key});
12 |
13 | @override
14 | Widget build(BuildContext context, WidgetRef ref) {
15 | final l10n = AppLocalizations.of(context);
16 | // This provider gets "downloaded" books from account (Cloud history)
17 | final downloadedBooksAsync = ref.watch(downloadedBooksProvider);
18 |
19 | return Scaffold(
20 | appBar: GradientAppBar(title: l10n.get('download_history')),
21 | body: downloadedBooksAsync.when(
22 | data: (books) {
23 | if (books.isEmpty) {
24 | return EmptyState(
25 | icon: Icons.history,
26 | title: l10n.get('no_history'),
27 | message: l10n.get('history_empty_message'),
28 | );
29 | }
30 |
31 | return ListView.builder(
32 | padding: const EdgeInsets.all(16),
33 | itemCount: books.length,
34 | itemBuilder: (context, index) {
35 | final book = books[index];
36 |
37 | return Card(
38 | margin: const EdgeInsets.only(bottom: 12),
39 | child: ListTile(
40 | contentPadding: const EdgeInsets.all(12),
41 | leading: book.cover != null
42 | ? ClipRRect(
43 | borderRadius: BorderRadius.circular(8),
44 | child: Image.network(
45 | book.cover!,
46 | width: 50,
47 | height: 70,
48 | fit: BoxFit.cover,
49 | ),
50 | )
51 | : const Icon(Icons.book, size: 50),
52 | title: Text(
53 | book.title,
54 | maxLines: 2,
55 | overflow: TextOverflow.ellipsis,
56 | ),
57 | subtitle: Column(
58 | crossAxisAlignment: CrossAxisAlignment.start,
59 | children: [
60 | if (book.author != null) Text(book.author!),
61 | if (book.filesizeString != null)
62 | Text(
63 | '${book.extension?.toUpperCase()} • ${book.filesizeString}',
64 | style: Theme.of(context).textTheme.bodySmall,
65 | ),
66 | ],
67 | ),
68 | trailing: const Icon(Icons.chevron_right),
69 | onTap: () {
70 | Navigator.of(context).pushNamed(
71 | AppRoutes.bookDetail,
72 | arguments: book,
73 | );
74 | },
75 | ),
76 | );
77 | },
78 | );
79 | },
80 | loading: () => LoadingWidget(message: l10n.get('loading_downloads')),
81 | error: (error, stack) => EmptyState(
82 | icon: Icons.error_outline,
83 | title: l10n.get('error'),
84 | message: error.toString(),
85 | ),
86 | ),
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutter_localizations/flutter_localizations.dart';
4 | import 'theme/app_theme.dart';
5 | import 'providers/settings_provider.dart';
6 | import 'routes/app_routes.dart';
7 | import 'screens/splash/splash_screen.dart';
8 | import 'screens/auth/login_screen.dart';
9 | // import 'screens/auth/register_screen.dart'; // Registration disabled - API不支持
10 | import 'screens/home/home_screen.dart';
11 | import 'screens/search/search_screen.dart';
12 | import 'screens/book_detail/book_detail_screen.dart';
13 | import 'screens/favorites/favorites_screen.dart';
14 | import 'screens/settings/history_screen.dart';
15 | import 'screens/downloads/local_downloads_screen.dart';
16 | import 'screens/settings/settings_screen.dart';
17 | import 'screens/similar/similar_books_screen.dart';
18 | import 'screens/reader/reader_screen.dart';
19 | import 'services/hive_service.dart';
20 | import 'services/ad_service.dart';
21 | import 'l10n/app_localizations.dart';
22 |
23 | void main() async {
24 | WidgetsFlutterBinding.ensureInitialized();
25 | await HiveService.init();
26 |
27 | // Initialize Unity Ads (non-blocking)
28 | AdService.init();
29 |
30 | runApp(
31 | const ProviderScope(
32 | child: MyApp(),
33 | ),
34 | );
35 | }
36 |
37 | class MyApp extends ConsumerWidget {
38 | const MyApp({super.key});
39 |
40 | @override
41 | Widget build(BuildContext context, WidgetRef ref) {
42 | final themeModeState = ref.watch(themeModeProvider);
43 | final locale = ref.watch(localeProvider);
44 |
45 | // Convert AppThemeMode to ThemeMode
46 | ThemeMode themeMode;
47 | switch (themeModeState) {
48 | case AppThemeMode.light:
49 | themeMode = ThemeMode.light;
50 | case AppThemeMode.dark:
51 | themeMode = ThemeMode.dark;
52 | case AppThemeMode.system:
53 | themeMode = ThemeMode.system;
54 | }
55 |
56 | return MaterialApp(
57 | title: 'Olib',
58 | debugShowCheckedModeBanner: false,
59 | theme: AppTheme.lightTheme,
60 | darkTheme: AppTheme.darkTheme,
61 | themeMode: themeMode,
62 |
63 | // Localization
64 | locale: locale,
65 | supportedLocales: supportedLocales,
66 | localizationsDelegates: const [
67 | AppLocalizations.delegate,
68 | GlobalMaterialLocalizations.delegate,
69 | GlobalWidgetsLocalizations.delegate,
70 | GlobalCupertinoLocalizations.delegate,
71 | ],
72 |
73 | initialRoute: AppRoutes.splash,
74 | routes: {
75 | AppRoutes.splash: (context) => const SplashScreen(),
76 | AppRoutes.login: (context) => const LoginScreen(),
77 | // AppRoutes.register: (context) => const RegisterScreen(), // Disabled
78 | AppRoutes.home: (context) => const HomeScreen(),
79 | AppRoutes.search: (context) => const SearchScreen(),
80 | AppRoutes.bookDetail: (context) => const BookDetailScreen(),
81 | AppRoutes.favorites: (context) => const FavoritesScreen(),
82 | AppRoutes.history: (context) => const HistoryScreen(),
83 | AppRoutes.downloads: (context) => const LocalDownloadsScreen(),
84 | AppRoutes.settings: (context) => const SettingsScreen(),
85 | AppRoutes.similarBooks: (context) => const SimilarBooksScreen(),
86 | AppRoutes.reader: (context) {
87 | final args = ModalRoute.of(context)!.settings.arguments as ReaderArgs;
88 | return ReaderScreen(url: args.url, title: args.title);
89 | },
90 | },
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/windows/runner/Runner.rc:
--------------------------------------------------------------------------------
1 | // Microsoft Visual C++ generated resource script.
2 | //
3 | #pragma code_page(65001)
4 | #include "resource.h"
5 |
6 | #define APSTUDIO_READONLY_SYMBOLS
7 | /////////////////////////////////////////////////////////////////////////////
8 | //
9 | // Generated from the TEXTINCLUDE 2 resource.
10 | //
11 | #include "winres.h"
12 |
13 | /////////////////////////////////////////////////////////////////////////////
14 | #undef APSTUDIO_READONLY_SYMBOLS
15 |
16 | /////////////////////////////////////////////////////////////////////////////
17 | // English (United States) resources
18 |
19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
21 |
22 | #ifdef APSTUDIO_INVOKED
23 | /////////////////////////////////////////////////////////////////////////////
24 | //
25 | // TEXTINCLUDE
26 | //
27 |
28 | 1 TEXTINCLUDE
29 | BEGIN
30 | "resource.h\0"
31 | END
32 |
33 | 2 TEXTINCLUDE
34 | BEGIN
35 | "#include ""winres.h""\r\n"
36 | "\0"
37 | END
38 |
39 | 3 TEXTINCLUDE
40 | BEGIN
41 | "\r\n"
42 | "\0"
43 | END
44 |
45 | #endif // APSTUDIO_INVOKED
46 |
47 |
48 | /////////////////////////////////////////////////////////////////////////////
49 | //
50 | // Icon
51 | //
52 |
53 | // Icon with lowest ID value placed first to ensure application icon
54 | // remains consistent on all systems.
55 | IDI_APP_ICON ICON "resources\\app_icon.ico"
56 |
57 |
58 | /////////////////////////////////////////////////////////////////////////////
59 | //
60 | // Version
61 | //
62 |
63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
65 | #else
66 | #define VERSION_AS_NUMBER 1,0,0,0
67 | #endif
68 |
69 | #if defined(FLUTTER_VERSION)
70 | #define VERSION_AS_STRING FLUTTER_VERSION
71 | #else
72 | #define VERSION_AS_STRING "1.0.0"
73 | #endif
74 |
75 | VS_VERSION_INFO VERSIONINFO
76 | FILEVERSION VERSION_AS_NUMBER
77 | PRODUCTVERSION VERSION_AS_NUMBER
78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
79 | #ifdef _DEBUG
80 | FILEFLAGS VS_FF_DEBUG
81 | #else
82 | FILEFLAGS 0x0L
83 | #endif
84 | FILEOS VOS__WINDOWS32
85 | FILETYPE VFT_APP
86 | FILESUBTYPE 0x0L
87 | BEGIN
88 | BLOCK "StringFileInfo"
89 | BEGIN
90 | BLOCK "040904e4"
91 | BEGIN
92 | VALUE "CompanyName", "Open Source Community" "\0"
93 | VALUE "FileDescription", "Olib" "\0"
94 | VALUE "FileVersion", VERSION_AS_STRING "\0"
95 | VALUE "InternalName", "olib" "\0"
96 | VALUE "LegalCopyright", "Copyright (C) 2025 Open Source Community. All rights reserved." "\0"
97 | VALUE "OriginalFilename", "olib.exe" "\0"
98 | VALUE "ProductName", "Olib" "\0"
99 | VALUE "ProductVersion", VERSION_AS_STRING "\0"
100 | END
101 | END
102 | BLOCK "VarFileInfo"
103 | BEGIN
104 | VALUE "Translation", 0x409, 1252
105 | END
106 | END
107 |
108 | #endif // English (United States) resources
109 | /////////////////////////////////////////////////////////////////////////////
110 |
111 |
112 |
113 | #ifndef APSTUDIO_INVOKED
114 | /////////////////////////////////////////////////////////////////////////////
115 | //
116 | // Generated from the TEXTINCLUDE 3 resource.
117 | //
118 |
119 |
120 | /////////////////////////////////////////////////////////////////////////////
121 | #endif // not APSTUDIO_INVOKED
122 |
--------------------------------------------------------------------------------
/lib/widgets/book_list_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../models/book.dart';
3 | import '../theme/app_colors.dart';
4 |
5 | /// Simplified list tile for books - compact view without cover images
6 | class BookListTile extends StatelessWidget {
7 | final Book book;
8 | final VoidCallback? onTap;
9 |
10 | const BookListTile({
11 | super.key,
12 | required this.book,
13 | this.onTap,
14 | });
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Card(
19 | margin: const EdgeInsets.only(bottom: 8),
20 | child: ListTile(
21 | contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
22 | leading: Container(
23 | width: 40,
24 | height: 40,
25 | decoration: BoxDecoration(
26 | color: AppColors.primary.withOpacity(0.1),
27 | borderRadius: BorderRadius.circular(8),
28 | ),
29 | child: const Icon(
30 | Icons.menu_book_rounded,
31 | color: AppColors.primary,
32 | size: 22,
33 | ),
34 | ),
35 | title: Text(
36 | book.title,
37 | maxLines: 1,
38 | overflow: TextOverflow.ellipsis,
39 | style: Theme.of(context).textTheme.titleSmall?.copyWith(
40 | fontWeight: FontWeight.w600,
41 | ),
42 | ),
43 | subtitle: Column(
44 | crossAxisAlignment: CrossAxisAlignment.start,
45 | children: [
46 | if (book.author != null && book.author!.isNotEmpty)
47 | Text(
48 | book.author!,
49 | maxLines: 1,
50 | overflow: TextOverflow.ellipsis,
51 | style: Theme.of(context).textTheme.bodySmall,
52 | ),
53 | const SizedBox(height: 4),
54 | Row(
55 | children: [
56 | // Extension badge
57 | if (book.extension != null && book.extension!.isNotEmpty)
58 | Container(
59 | padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
60 | decoration: BoxDecoration(
61 | color: AppColors.accent.withOpacity(0.15),
62 | borderRadius: BorderRadius.circular(4),
63 | ),
64 | child: Text(
65 | book.extension!.toUpperCase(),
66 | style: const TextStyle(
67 | color: AppColors.accent,
68 | fontSize: 10,
69 | fontWeight: FontWeight.bold,
70 | ),
71 | ),
72 | ),
73 | if (book.extension != null && book.filesizeString != null)
74 | const SizedBox(width: 8),
75 | // File size
76 | if (book.filesizeString != null)
77 | Text(
78 | book.filesizeString!,
79 | style: TextStyle(
80 | color: AppColors.textSecondary,
81 | fontSize: 11,
82 | ),
83 | ),
84 | const Spacer(),
85 | // Year
86 | if (book.year != null && book.year != 0)
87 | Text(
88 | '${book.year}',
89 | style: TextStyle(
90 | color: AppColors.textSecondary,
91 | fontSize: 11,
92 | ),
93 | ),
94 | ],
95 | ),
96 | ],
97 | ),
98 | trailing: const Icon(Icons.chevron_right, color: AppColors.textSecondary),
99 | onTap: onTap,
100 | ),
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/lib/screens/reader/reader_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_inappwebview/flutter_inappwebview.dart';
3 | import '../../theme/app_colors.dart';
4 |
5 | class ReaderScreen extends StatefulWidget {
6 | final String url;
7 | final String title;
8 |
9 | const ReaderScreen({
10 | super.key,
11 | required this.url,
12 | required this.title,
13 | });
14 |
15 | @override
16 | State createState() => _ReaderScreenState();
17 | }
18 |
19 | class _ReaderScreenState extends State {
20 | InAppWebViewController? _webViewController;
21 | double _progress = 0;
22 | bool _isLoading = true;
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return Scaffold(
27 | appBar: AppBar(
28 | title: Text(
29 | widget.title,
30 | maxLines: 1,
31 | overflow: TextOverflow.ellipsis,
32 | ),
33 | backgroundColor: AppColors.primary,
34 | foregroundColor: Colors.white,
35 | actions: [
36 | IconButton(
37 | icon: const Icon(Icons.refresh),
38 | onPressed: () => _webViewController?.reload(),
39 | ),
40 | ],
41 | ),
42 | body: Stack(
43 | children: [
44 | InAppWebView(
45 | initialUrlRequest: URLRequest(url: WebUri(widget.url)),
46 | initialSettings: InAppWebViewSettings(
47 | javaScriptEnabled: true,
48 | domStorageEnabled: true,
49 | databaseEnabled: true,
50 | useWideViewPort: true,
51 | loadWithOverviewMode: true,
52 | supportZoom: true,
53 | builtInZoomControls: true,
54 | displayZoomControls: false,
55 | mediaPlaybackRequiresUserGesture: false,
56 | allowsInlineMediaPlayback: true,
57 | useShouldOverrideUrlLoading: true,
58 | userAgent: 'Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
59 | ),
60 | onWebViewCreated: (controller) {
61 | _webViewController = controller;
62 | },
63 | onLoadStart: (controller, url) {
64 | setState(() {
65 | _isLoading = true;
66 | });
67 | },
68 | onLoadStop: (controller, url) async {
69 | setState(() {
70 | _isLoading = false;
71 | });
72 | },
73 | onProgressChanged: (controller, progress) {
74 | setState(() {
75 | _progress = progress / 100;
76 | });
77 | },
78 | shouldOverrideUrlLoading: (controller, navigationAction) async {
79 | // Allow all navigation
80 | return NavigationActionPolicy.ALLOW;
81 | },
82 | onReceivedError: (controller, request, error) {
83 | debugPrint('WebView error: ${error.description}');
84 | },
85 | ),
86 |
87 | // Progress indicator
88 | if (_isLoading)
89 | Positioned(
90 | top: 0,
91 | left: 0,
92 | right: 0,
93 | child: LinearProgressIndicator(
94 | value: _progress,
95 | backgroundColor: Colors.grey[200],
96 | valueColor: const AlwaysStoppedAnimation(AppColors.primary),
97 | ),
98 | ),
99 | ],
100 | ),
101 | );
102 | }
103 | }
104 |
105 | /// Arguments for ReaderScreen
106 | class ReaderArgs {
107 | final String url;
108 | final String title;
109 |
110 | const ReaderArgs({
111 | required this.url,
112 | required this.title,
113 | });
114 | }
115 |
--------------------------------------------------------------------------------
/windows/runner/win32_window.h:
--------------------------------------------------------------------------------
1 | #ifndef RUNNER_WIN32_WINDOW_H_
2 | #define RUNNER_WIN32_WINDOW_H_
3 |
4 | #include
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be
11 | // inherited from by classes that wish to specialize with custom
12 | // rendering and input handling
13 | class Win32Window {
14 | public:
15 | struct Point {
16 | unsigned int x;
17 | unsigned int y;
18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {}
19 | };
20 |
21 | struct Size {
22 | unsigned int width;
23 | unsigned int height;
24 | Size(unsigned int width, unsigned int height)
25 | : width(width), height(height) {}
26 | };
27 |
28 | Win32Window();
29 | virtual ~Win32Window();
30 |
31 | // Creates a win32 window with |title| that is positioned and sized using
32 | // |origin| and |size|. New windows are created on the default monitor. Window
33 | // sizes are specified to the OS in physical pixels, hence to ensure a
34 | // consistent size this function will scale the inputted width and height as
35 | // as appropriate for the default monitor. The window is invisible until
36 | // |Show| is called. Returns true if the window was created successfully.
37 | bool Create(const std::wstring& title, const Point& origin, const Size& size);
38 |
39 | // Show the current window. Returns true if the window was successfully shown.
40 | bool Show();
41 |
42 | // Release OS resources associated with window.
43 | void Destroy();
44 |
45 | // Inserts |content| into the window tree.
46 | void SetChildContent(HWND content);
47 |
48 | // Returns the backing Window handle to enable clients to set icon and other
49 | // window properties. Returns nullptr if the window has been destroyed.
50 | HWND GetHandle();
51 |
52 | // If true, closing this window will quit the application.
53 | void SetQuitOnClose(bool quit_on_close);
54 |
55 | // Return a RECT representing the bounds of the current client area.
56 | RECT GetClientArea();
57 |
58 | protected:
59 | // Processes and route salient window messages for mouse handling,
60 | // size change and DPI. Delegates handling of these to member overloads that
61 | // inheriting classes can handle.
62 | virtual LRESULT MessageHandler(HWND window,
63 | UINT const message,
64 | WPARAM const wparam,
65 | LPARAM const lparam) noexcept;
66 |
67 | // Called when CreateAndShow is called, allowing subclass window-related
68 | // setup. Subclasses should return false if setup fails.
69 | virtual bool OnCreate();
70 |
71 | // Called when Destroy is called.
72 | virtual void OnDestroy();
73 |
74 | private:
75 | friend class WindowClassRegistrar;
76 |
77 | // OS callback called by message pump. Handles the WM_NCCREATE message which
78 | // is passed when the non-client area is being created and enables automatic
79 | // non-client DPI scaling so that the non-client area automatically
80 | // responds to changes in DPI. All other messages are handled by
81 | // MessageHandler.
82 | static LRESULT CALLBACK WndProc(HWND const window,
83 | UINT const message,
84 | WPARAM const wparam,
85 | LPARAM const lparam) noexcept;
86 |
87 | // Retrieves a class instance pointer for |window|
88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept;
89 |
90 | // Update the window frame's theme to match the system theme.
91 | static void UpdateTheme(HWND const window);
92 |
93 | bool quit_on_close_ = false;
94 |
95 | // window handle for top level window.
96 | HWND window_handle_ = nullptr;
97 |
98 | // window handle for hosted content.
99 | HWND child_content_ = nullptr;
100 | };
101 |
102 | #endif // RUNNER_WIN32_WINDOW_H_
103 |
--------------------------------------------------------------------------------
/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
64 |
66 |
72 |
73 |
74 |
75 |
81 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/windows/flutter/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # This file controls Flutter-level build steps. It should not be edited.
2 | cmake_minimum_required(VERSION 3.14)
3 |
4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
5 |
6 | # Configuration provided via flutter tool.
7 | include(${EPHEMERAL_DIR}/generated_config.cmake)
8 |
9 | # TODO: Move the rest of this into files in ephemeral. See
10 | # https://github.com/flutter/flutter/issues/57146.
11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
12 |
13 | # Set fallback configurations for older versions of the flutter tool.
14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
15 | set(FLUTTER_TARGET_PLATFORM "windows-x64")
16 | endif()
17 |
18 | # === Flutter Library ===
19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
20 |
21 | # Published to parent scope for install step.
22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
26 |
27 | list(APPEND FLUTTER_LIBRARY_HEADERS
28 | "flutter_export.h"
29 | "flutter_windows.h"
30 | "flutter_messenger.h"
31 | "flutter_plugin_registrar.h"
32 | "flutter_texture_registrar.h"
33 | )
34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
35 | add_library(flutter INTERFACE)
36 | target_include_directories(flutter INTERFACE
37 | "${EPHEMERAL_DIR}"
38 | )
39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
40 | add_dependencies(flutter flutter_assemble)
41 |
42 | # === Wrapper ===
43 | list(APPEND CPP_WRAPPER_SOURCES_CORE
44 | "core_implementations.cc"
45 | "standard_codec.cc"
46 | )
47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
49 | "plugin_registrar.cc"
50 | )
51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
52 | list(APPEND CPP_WRAPPER_SOURCES_APP
53 | "flutter_engine.cc"
54 | "flutter_view_controller.cc"
55 | )
56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
57 |
58 | # Wrapper sources needed for a plugin.
59 | add_library(flutter_wrapper_plugin STATIC
60 | ${CPP_WRAPPER_SOURCES_CORE}
61 | ${CPP_WRAPPER_SOURCES_PLUGIN}
62 | )
63 | apply_standard_settings(flutter_wrapper_plugin)
64 | set_target_properties(flutter_wrapper_plugin PROPERTIES
65 | POSITION_INDEPENDENT_CODE ON)
66 | set_target_properties(flutter_wrapper_plugin PROPERTIES
67 | CXX_VISIBILITY_PRESET hidden)
68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
69 | target_include_directories(flutter_wrapper_plugin PUBLIC
70 | "${WRAPPER_ROOT}/include"
71 | )
72 | add_dependencies(flutter_wrapper_plugin flutter_assemble)
73 |
74 | # Wrapper sources needed for the runner.
75 | add_library(flutter_wrapper_app STATIC
76 | ${CPP_WRAPPER_SOURCES_CORE}
77 | ${CPP_WRAPPER_SOURCES_APP}
78 | )
79 | apply_standard_settings(flutter_wrapper_app)
80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter)
81 | target_include_directories(flutter_wrapper_app PUBLIC
82 | "${WRAPPER_ROOT}/include"
83 | )
84 | add_dependencies(flutter_wrapper_app flutter_assemble)
85 |
86 | # === Flutter tool backend ===
87 | # _phony_ is a non-existent file to force this command to run every time,
88 | # since currently there's no way to get a full input/output list from the
89 | # flutter tool.
90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
92 | add_custom_command(
93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
95 | ${CPP_WRAPPER_SOURCES_APP}
96 | ${PHONY_OUTPUT}
97 | COMMAND ${CMAKE_COMMAND} -E env
98 | ${FLUTTER_TOOL_ENVIRONMENT}
99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
100 | ${FLUTTER_TARGET_PLATFORM} $
101 | VERBATIM
102 | )
103 | add_custom_target(flutter_assemble DEPENDS
104 | "${FLUTTER_LIBRARY}"
105 | ${FLUTTER_LIBRARY_HEADERS}
106 | ${CPP_WRAPPER_SOURCES_CORE}
107 | ${CPP_WRAPPER_SOURCES_PLUGIN}
108 | ${CPP_WRAPPER_SOURCES_APP}
109 | )
110 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
38 |
39 |
40 |
41 |
44 |
50 |
51 |
52 |
53 |
54 |
66 |
68 |
74 |
75 |
76 |
77 |
83 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📚 Olib
2 |
3 |
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 | **🤖 An open-source ebook reader built entirely with AI assistance**
12 |
13 | **Third-party client • Frontend interface only • All data from external sources**
14 |
15 | [Download](https://bookbook.space) • [Report Bug](../../issues) • [Request Feature](../../issues)
16 |
17 | **English** | **[简体中文](README_ZH.md)** | **[日本語](README_JA.md)** | **[한국어](README_KO.md)**
18 |
19 |
20 |
21 | ---
22 |
23 | > ⚠️ **Disclaimer**: Olib is an independent, open-source third-party client. It is NOT an official client and is not affiliated with any official service. This project only provides the frontend interface - all book data comes from external sources. Use at your own discretion.
24 |
25 | ## ✨ Features
26 |
27 | | Feature | Description |
28 | |---------|-------------|
29 | | 📖 **Book Search** | Search books by title, author, ISBN, or keywords |
30 | | 💾 **Offline Reading** | Download books for reading without internet |
31 | | 🌙 **Dark Mode** | Eye-friendly reading experience |
32 | | 🌍 **Multi-language** | Supports 16+ languages including English, 中文, 日本語, 한국어 |
33 | | 🔐 **Multi-Account** | Switch between multiple accounts seamlessly |
34 | | 🔗 **Multi-Domain** | Choose from multiple server lines |
35 | | 🆓 **100% Free** | No ads, no subscriptions, no hidden costs |
36 |
37 | ## 🤖 AI-Built Project
38 |
39 | This project was built **entirely with AI assistance**:
40 | - Architecture design by AI
41 | - Code implementation by AI
42 | - UI/UX design by AI
43 | - Documentation by AI
44 |
45 | ## 📱 Screenshots
46 |
47 |
48 | Coming soon...
49 |
50 |
51 | ## 🚀 Quick Start
52 |
53 | ### Prerequisites
54 |
55 | - Flutter SDK 3.8+
56 | - Android Studio / VS Code
57 | - Android device or emulator
58 |
59 | ### Installation
60 |
61 | ```bash
62 | # Clone the repository
63 | git clone https://github.com/shiyi-0x7f/olib-mobile.git
64 |
65 | # Navigate to project directory
66 | cd olib-mobile
67 |
68 | # Install dependencies
69 | flutter pub get
70 |
71 | # Run the app
72 | flutter run
73 | ```
74 |
75 | ### Build APK
76 |
77 | ```bash
78 | flutter build apk --release
79 | ```
80 |
81 | ## 🏗️ Project Structure
82 |
83 | ```
84 | lib/
85 | ├── l10n/ # Localization files (16+ languages)
86 | ├── models/ # Data models
87 | ├── providers/ # State management (Riverpod)
88 | ├── routes/ # App navigation
89 | ├── screens/ # UI screens
90 | ├── services/ # API & storage services
91 | ├── theme/ # App theme configuration
92 | └── widgets/ # Reusable components
93 | ```
94 |
95 | ## 🛠️ Tech Stack
96 |
97 | - **Framework**: Flutter
98 | - **State Management**: Riverpod
99 | - **Local Storage**: Hive
100 | - **HTTP Client**: http package
101 | - **Localization**: 16+ languages
102 |
103 | ## 🤝 Contributing
104 |
105 | Contributions are welcome! Please feel free to submit a Pull Request.
106 |
107 | 1. Fork the Project
108 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
109 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
110 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
111 | 5. Open a Pull Request
112 |
113 | ## 📄 License
114 |
115 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
116 |
117 | > ⚠️ **Legal Notice**:
118 | > - This is an independent third-party client, NOT an official application
119 | > - All book data comes from external sources; this project provides frontend only
120 | > - Users are responsible for ensuring compliance with applicable laws
121 | > - By using this software, you acknowledge these terms
122 |
123 | ## 💖 Acknowledgments
124 |
125 | - 🤖 Built with AI assistance
126 | - 💙 Flutter framework
127 | - ❤️ Open source community
128 |
129 | ---
130 |
131 |
132 |
133 | **[⬆ Back to Top](#-olib)**
134 |
135 | Built with 🤖 AI • Open Source • Free Forever
136 |
137 |
138 |
--------------------------------------------------------------------------------
/windows/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # Project-level configuration.
2 | cmake_minimum_required(VERSION 3.14)
3 | project(olib LANGUAGES CXX)
4 |
5 | # The name of the executable created for the application. Change this to change
6 | # the on-disk name of your application.
7 | set(BINARY_NAME "olib")
8 |
9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
10 | # versions of CMake.
11 | cmake_policy(VERSION 3.14...3.25)
12 |
13 | # Define build configuration option.
14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
15 | if(IS_MULTICONFIG)
16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
17 | CACHE STRING "" FORCE)
18 | else()
19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
20 | set(CMAKE_BUILD_TYPE "Debug" CACHE
21 | STRING "Flutter build mode" FORCE)
22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
23 | "Debug" "Profile" "Release")
24 | endif()
25 | endif()
26 | # Define settings for the Profile build mode.
27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
31 |
32 | # Use Unicode for all projects.
33 | add_definitions(-DUNICODE -D_UNICODE)
34 |
35 | # Compilation settings that should be applied to most targets.
36 | #
37 | # Be cautious about adding new options here, as plugins use this function by
38 | # default. In most cases, you should add new options to specific targets instead
39 | # of modifying this function.
40 | function(APPLY_STANDARD_SETTINGS TARGET)
41 | target_compile_features(${TARGET} PUBLIC cxx_std_17)
42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
43 | target_compile_options(${TARGET} PRIVATE /EHsc)
44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
46 | endfunction()
47 |
48 | # Flutter library and tool build rules.
49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
50 | add_subdirectory(${FLUTTER_MANAGED_DIR})
51 |
52 | # Application build; see runner/CMakeLists.txt.
53 | add_subdirectory("runner")
54 |
55 |
56 | # Generated plugin build rules, which manage building the plugins and adding
57 | # them to the application.
58 | include(flutter/generated_plugins.cmake)
59 |
60 |
61 | # === Installation ===
62 | # Support files are copied into place next to the executable, so that it can
63 | # run in place. This is done instead of making a separate bundle (as on Linux)
64 | # so that building and running from within Visual Studio will work.
65 | set(BUILD_BUNDLE_DIR "$")
66 | # Make the "install" step default, as it's required to run.
67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
70 | endif()
71 |
72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
74 |
75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
76 | COMPONENT Runtime)
77 |
78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
79 | COMPONENT Runtime)
80 |
81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
82 | COMPONENT Runtime)
83 |
84 | if(PLUGIN_BUNDLED_LIBRARIES)
85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
87 | COMPONENT Runtime)
88 | endif()
89 |
90 | # Copy the native assets provided by the build.dart from all packages.
91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}"
93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
94 | COMPONENT Runtime)
95 |
96 | # Fully re-copy the assets directory on each build to avoid having stale files
97 | # from a previous install.
98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
99 | install(CODE "
100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
101 | " COMPONENT Runtime)
102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
104 |
105 | # Install the AOT library on non-Debug builds only.
106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
107 | CONFIGURATIONS Profile;Release
108 | COMPONENT Runtime)
109 |
--------------------------------------------------------------------------------
/lib/screens/favorites/favorites_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutter_slidable/flutter_slidable.dart';
4 | import '../../providers/books_provider.dart';
5 | import '../../widgets/gradient_app_bar.dart';
6 | import '../../widgets/loading_widget.dart';
7 | import '../../widgets/empty_state.dart';
8 | import '../../routes/app_routes.dart';
9 | import '../../theme/app_colors.dart';
10 | import '../../l10n/app_localizations.dart';
11 |
12 | class FavoritesScreen extends ConsumerWidget {
13 | const FavoritesScreen({super.key});
14 |
15 | @override
16 | Widget build(BuildContext context, WidgetRef ref) {
17 | final savedBooksAsync = ref.watch(savedBooksProvider);
18 |
19 | return Scaffold(
20 | appBar: GradientAppBar(title: AppLocalizations.of(context).get('favorites')),
21 | body: savedBooksAsync.when(
22 | data: (books) {
23 | if (books.isEmpty) {
24 | return EmptyState(
25 | icon: Icons.favorite_outline,
26 | title: AppLocalizations.of(context).get('no_favorites'),
27 | message: AppLocalizations.of(context).get('save_books_hint'),
28 | );
29 | }
30 |
31 | return ListView.builder(
32 | padding: const EdgeInsets.all(16),
33 | itemCount: books.length,
34 | itemBuilder: (context, index) {
35 | final book = books[index];
36 |
37 | return Slidable(
38 | key: ValueKey(book.id),
39 | endActionPane: ActionPane(
40 | motion: const ScrollMotion(),
41 | children: [
42 | SlidableAction(
43 | onPressed: (context) async {
44 | await ref
45 | .read(savedBooksProvider.notifier)
46 | .unsaveBook(book.id.toString());
47 | },
48 | backgroundColor: AppColors.error,
49 | foregroundColor: Colors.white,
50 | icon: Icons.delete,
51 | label: AppLocalizations.of(context).get('remove'),
52 | ),
53 | ],
54 | ),
55 | child: Card(
56 | margin: const EdgeInsets.only(bottom: 12),
57 | child: ListTile(
58 | contentPadding: const EdgeInsets.all(12),
59 | leading: book.cover != null
60 | ? ClipRRect(
61 | borderRadius: BorderRadius.circular(8),
62 | child: Image.network(
63 | book.cover!,
64 | width: 50,
65 | height: 70,
66 | fit: BoxFit.cover,
67 | ),
68 | )
69 | : Container(
70 | width: 50,
71 | height: 70,
72 | decoration: BoxDecoration(
73 | color: AppColors.textSecondary.withOpacity(0.1),
74 | borderRadius: BorderRadius.circular(8),
75 | ),
76 | child: const Icon(Icons.book),
77 | ),
78 | title: Text(
79 | book.title,
80 | maxLines: 2,
81 | overflow: TextOverflow.ellipsis,
82 | ),
83 | subtitle: book.author != null
84 | ? Text(book.author!)
85 | : null,
86 | trailing: const Icon(Icons.chevron_right),
87 | onTap: () {
88 | Navigator.of(context).pushNamed(
89 | AppRoutes.bookDetail,
90 | arguments: book,
91 | );
92 | },
93 | ),
94 | ),
95 | );
96 | },
97 | );
98 | },
99 | loading: () => LoadingWidget(message: AppLocalizations.of(context).get('loading_favorites')),
100 | error: (error, stack) => EmptyState(
101 | icon: Icons.error_outline,
102 | title: AppLocalizations.of(context).get('error'),
103 | message: error.toString(),
104 | ),
105 | ),
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/services/update_service.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:flutter/foundation.dart';
3 | import 'package:http/http.dart' as http;
4 | import 'package:package_info_plus/package_info_plus.dart';
5 | import 'hive_service.dart';
6 |
7 | /// Update checker service for checking new app versions
8 | class UpdateService {
9 | static const String _versionUrl = 'https://bookbook.space/version.json';
10 | static const String _lastCheckKey = 'last_update_check';
11 | static const String _dismissedVersionKey = 'dismissed_version';
12 |
13 | /// Check interval: once per day
14 | static const Duration _checkInterval = Duration(hours: 24);
15 |
16 | /// Remote version info
17 | static String? latestVersion;
18 | static String? downloadUrl;
19 | static Map? changelog;
20 | static bool forceUpdate = false;
21 | static bool hasUpdate = false;
22 |
23 | /// Flag to indicate app is blocked due to force update
24 | /// When true, search and download features should be disabled
25 | static bool isBlocked = false;
26 |
27 | /// Check for updates (non-blocking, silent on errors)
28 | static Future checkForUpdate({bool force = false}) async {
29 | try {
30 | // Check if we should skip (already checked recently)
31 | if (!force && !_shouldCheck()) {
32 | debugPrint('UpdateService: Skipping check (checked recently)');
33 | return false;
34 | }
35 |
36 | // Fetch remote version
37 | final response = await http.get(
38 | Uri.parse(_versionUrl),
39 | ).timeout(const Duration(seconds: 10));
40 |
41 | if (response.statusCode != 200) {
42 | debugPrint('UpdateService: Failed to fetch version (${response.statusCode})');
43 | return false;
44 | }
45 |
46 | final data = json.decode(response.body) as Map;
47 | latestVersion = data['version'] as String?;
48 | downloadUrl = data['url'] as String?;
49 | forceUpdate = data['force_update'] as bool? ?? false;
50 |
51 | // Parse changelog
52 | if (data['changelog'] != null) {
53 | final changelogData = data['changelog'] as Map;
54 | changelog = changelogData.map((k, v) => MapEntry(k, v.toString()));
55 | }
56 |
57 | // Get current version
58 | final packageInfo = await PackageInfo.fromPlatform();
59 | final currentVersion = packageInfo.version;
60 |
61 | // Compare versions
62 | hasUpdate = _isNewerVersion(latestVersion!, currentVersion);
63 |
64 | // Save check time
65 | await HiveService.settingsBox.put(
66 | _lastCheckKey,
67 | DateTime.now().millisecondsSinceEpoch,
68 | );
69 |
70 | debugPrint('UpdateService: Current=$currentVersion, Latest=$latestVersion, HasUpdate=$hasUpdate');
71 |
72 | return hasUpdate;
73 | } catch (e) {
74 | debugPrint('UpdateService: Error checking for update: $e');
75 | return false;
76 | }
77 | }
78 |
79 | /// Check if we should perform update check
80 | static bool _shouldCheck() {
81 | final lastCheck = HiveService.settingsBox.get(_lastCheckKey);
82 | if (lastCheck == null) return true;
83 |
84 | final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheck as int);
85 | return DateTime.now().difference(lastCheckTime) > _checkInterval;
86 | }
87 |
88 | /// Compare version strings (e.g., "1.0.1" > "1.0.0")
89 | static bool _isNewerVersion(String remote, String current) {
90 | final remoteParts = remote.split('.').map(int.parse).toList();
91 | final currentParts = current.split('.').map(int.parse).toList();
92 |
93 | for (int i = 0; i < remoteParts.length && i < currentParts.length; i++) {
94 | if (remoteParts[i] > currentParts[i]) return true;
95 | if (remoteParts[i] < currentParts[i]) return false;
96 | }
97 |
98 | return remoteParts.length > currentParts.length;
99 | }
100 |
101 | /// Check if user has dismissed this version
102 | static bool isVersionDismissed() {
103 | final dismissed = HiveService.settingsBox.get(_dismissedVersionKey);
104 | return dismissed == latestVersion;
105 | }
106 |
107 | /// Dismiss the current update notification
108 | static Future dismissUpdate() async {
109 | if (latestVersion != null) {
110 | await HiveService.settingsBox.put(_dismissedVersionKey, latestVersion);
111 | }
112 | }
113 |
114 | /// Get changelog text for current locale
115 | static String getChangelog(String locale) {
116 | if (changelog == null) return '';
117 | return changelog![locale] ?? changelog!['en'] ?? '';
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/lib/services/storage_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:shared_preferences/shared_preferences.dart';
2 | import 'dart:convert';
3 |
4 | class StorageService {
5 | static const String _keyFavorites = 'favorite_books';
6 | static const String _keyDownloads = 'downloaded_books';
7 | static const String _keyThemeMode = 'theme_mode';
8 | static const String _keyDownloadPath = 'download_path';
9 | static const String _keyDownloadHistory = 'download_history';
10 |
11 | /// Save favorite book IDs
12 | Future saveFavorites(List bookIds) async {
13 | final prefs = await SharedPreferences.getInstance();
14 | await prefs.setStringList(
15 | _keyFavorites,
16 | bookIds,
17 | );
18 | }
19 |
20 | /// Get favorite book IDs
21 | Future> getFavorites() async {
22 | final prefs = await SharedPreferences.getInstance();
23 | return prefs.getStringList(_keyFavorites) ?? [];
24 | }
25 |
26 | /// Add book to favorites
27 | Future addFavorite(String bookId) async {
28 | final favorites = await getFavorites();
29 | if (!favorites.contains(bookId)) {
30 | favorites.add(bookId);
31 | await saveFavorites(favorites);
32 | }
33 | }
34 |
35 | /// Remove book from favorites
36 | Future removeFavorite(String bookId) async {
37 | final favorites = await getFavorites();
38 | favorites.remove(bookId);
39 | await saveFavorites(favorites);
40 | }
41 |
42 | /// Check if book is favorited
43 | Future isFavorite(String bookId) async {
44 | final favorites = await getFavorites();
45 | return favorites.contains(bookId);
46 | }
47 |
48 | /// Save downloaded book info
49 | Future saveDownloadedBook(Map bookInfo) async {
50 | final prefs = await SharedPreferences.getInstance();
51 | final downloads = prefs.getStringList(_keyDownloads) ?? [];
52 | // Store as JSON string
53 | downloads.add(bookInfo.toString());
54 | await prefs.setStringList(_keyDownloads, downloads);
55 | }
56 |
57 | /// Get theme mode (0: system, 1: light, 2: dark)
58 | Future getThemeMode() async {
59 | final prefs = await SharedPreferences.getInstance();
60 | return prefs.getInt(_keyThemeMode) ?? 0;
61 | }
62 |
63 | /// Set theme mode
64 | Future setThemeMode(int mode) async {
65 | final prefs = await SharedPreferences.getInstance();
66 | await prefs.setInt(_keyThemeMode, mode);
67 | }
68 |
69 | /// Get download path
70 | Future getDownloadPath() async {
71 | final prefs = await SharedPreferences.getInstance();
72 | return prefs.getString(_keyDownloadPath);
73 | }
74 |
75 | /// Set download path
76 | Future setDownloadPath(String path) async {
77 | final prefs = await SharedPreferences.getInstance();
78 | await prefs.setString(_keyDownloadPath, path);
79 | }
80 |
81 | // ===== Download History Methods =====
82 |
83 | /// Add book to download history
84 | /// Stores: {bookId: {title, author, filePath, cover, extension, downloadTime}}
85 | Future addToDownloadHistory(
86 | String bookId,
87 | String title,
88 | String? author,
89 | String filePath,
90 | {String? cover, String? extension}
91 | ) async {
92 | final prefs = await SharedPreferences.getInstance();
93 | final historyJson = prefs.getString(_keyDownloadHistory) ?? '{}';
94 | final history = Map.from(jsonDecode(historyJson));
95 |
96 | history[bookId] = {
97 | 'title': title,
98 | 'author': author,
99 | 'filePath': filePath,
100 | 'cover': cover,
101 | 'extension': extension,
102 | 'downloadTime': DateTime.now().toIso8601String(),
103 | };
104 |
105 | await prefs.setString(_keyDownloadHistory, jsonEncode(history));
106 | }
107 |
108 | /// Get all download history
109 | Future