Skip to content

Y pl dev#715

Closed
Sivan22 wants to merge 219 commits intomainfrom
Y-PL-DEV
Closed

Y pl dev#715
Sivan22 wants to merge 219 commits intomainfrom
Y-PL-DEV

Conversation

@Sivan22
Copy link
Owner

@Sivan22 Sivan22 commented Sep 8, 2025

Summary by CodeRabbit

  • New Features
    • Personal Notes: sidebar, editor, highlights, orphan manager, import/export, performance dashboard.
    • Calendar (עוד): full Hebrew/Gregorian calendar with events and Daf Yomi (Bavli/Yerushalmi).
    • History: searchable list, open previous searches/books, bulk/flush controls.
    • Bookmarks: new dialog and in-list search.
  • Enhancements
    • Library: header with sync, refresh, history, bookmarks; preserves category on refresh.
    • Search: advanced modes, regex options, improved facet counts.
    • PDF reader: resizable left sidebar, refined outline/thumbnails, print action.
    • Single-instance app; desktop DB support for notes.
  • Documentation
    • New user guide, API reference, and Hebrew morphology search guide.

Y-PLONI added 30 commits July 15, 2025 01:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
lib/empty_library/bloc/empty_library_bloc.dart (5)

218-257: Zip‑Slip vulnerability when extracting entries; sanitize archive paths and constrain to destination.

file.name can contain ../ or absolute paths allowing writes outside libraryPath. Validate and normalize paths before writing.

Apply this diff (also add the import in the imports section):

+import 'package:path/path.dart' as p
-              for (final file in archive.files) {
+              for (final file in archive.files) {
                 if (!_isCancelling) {
                   final filename = file.name;
-                  final filePath = '$libraryPath/$filename';
+                  final resolvedPath = p.normalize(p.join(libraryPath, filename));
+                  if (!p.isWithin(libraryPath, resolvedPath)) {
+                    throw Exception('Invalid path in archive (zip‑slip): $filename');
+                  }
 
                   add(DownloadProgressUpdated(
                       progress: extractedFiles / totalFiles,
                       currentOperation: 'מחלץ: $filename',
                       downloadedMB: state.downloadedMB,
                       downloadSpeed: state.downloadSpeed));
 
                   try {
                     if (file.isFile) {
-                      final outputFile = File(filePath);
+                      final outputFile = File(resolvedPath);
                       await outputFile.parent.create(recursive: true);
-                      final outputStream = OutputFileStream(outputFile.path);
-                      file.writeContent(outputStream);
-                      outputStream.close();
+                      final outputStream = OutputFileStream(outputFile.path);
+                      try {
+                        file.writeContent(outputStream);
+                      } finally {
+                        outputStream.close();
+                      }
                     } else {
-                      await Directory(filePath).create(recursive: true);
+                      await Directory(resolvedPath).create(recursive: true);
                     }
                     extractedFiles++;
                   } catch (e) {
                     print('Error extracting $filename: $e');
                     throw Exception('שגיאה בחילוץ הקובץ $filename: $e');
                   }
                 }
               }
-              inputStream.close();
+              inputStream.close();

260-272: Also sanitize paths when using flutter_archive; skip unsafe entries.

Guard zipEntry.name the same way and skip items outside libraryPath.

Apply this diff:

-                    onExtracting: (zipEntry, progress) {
-                      add(DownloadProgressUpdated(
+                    onExtracting: (zipEntry, progress) {
+                      final resolvedPath = p.normalize(p.join(libraryPath, zipEntry.name));
+                      if (!p.isWithin(libraryPath, resolvedPath)) {
+                        return flutter_archive.ZipFileOperation.skipItem;
+                      }
+                      add(DownloadProgressUpdated(
                           progress: progress,
                           currentOperation: 'מחלץ: ${zipEntry.name}',
                           downloadedMB: state.downloadedMB,
                           downloadSpeed: state.downloadSpeed));
                       return flutter_archive.ZipFileOperation.includeItem;
                     });

160-171: HTTP client not closed; no timeouts; brittle status check.

http.Client() is never closed (socket leak) and the request has no timeout. Also, accept any 2xx, not only 200.

Apply this diff:

-      final request = http.Request(
-        'GET',
-        Uri.parse(
-            'https://github.com/zevisvei/otzaria-library/releases/download/latest/otzaria_latest.zip'),
-      );
-      final response = await http.Client().send(request);
+      final client = http.Client();
+      final request = http.Request(
+        'GET',
+        Uri.parse('https://github.com/zevisvei/otzaria-library/releases/download/latest/otzaria_latest.zip'),
+      )..headers.addAll({
+          'Accept': 'application/octet-stream',
+        });
+      final response = await client.send(request).timeout(const Duration(minutes: 5));
 
-      if (response.statusCode != 200) {
+      if (response.statusCode < 200 || response.statusCode >= 300) {
         throw Exception('Failed to start download: ${response.statusCode}');
       }

And ensure cleanup on both success and error:

         onDone: () async {
           if (_isCancelling) return;
+          client.close();
           await _fileSink?.flush();
           await _fileSink?.close();
           _fileSink = null;
-        onError: (error) {
-          emit(EmptyLibraryError(errorMessage: 'שגיאה בהורדה: $error'));
-        },
+        onError: (error) async {
+          try {
+            await _fileSink?.flush();
+          } catch (_) {}
+          await _fileSink?.close();
+          _fileSink = null;
+          _speedTimer?.cancel();
+          client.close();
+          await _cleanupTempFile();
+          emit(EmptyLibraryError(errorMessage: 'שגיאה בהורדה: $error'));
+        },

Also applies to: 290-293


221-257: Close streams with try/finally to avoid handle leaks if exceptions occur mid‑extract.

inputStream.close() and outputStream.close() should be in finally blocks.

Apply this diff:

-              final inputStream = InputFileStream(_tempFile!.path);
-              final archive = extractor.decodeBuffer(inputStream);
+              final inputStream = InputFileStream(_tempFile!.path);
+              try {
+                final archive = extractor.decodeBuffer(inputStream);
                 ...
-                      final outputStream = OutputFileStream(outputFile.path);
-                      try {
-                        file.writeContent(outputStream);
-                      } finally {
-                        outputStream.close();
-                      }
+                      final outputStream = OutputFileStream(outputFile.path);
+                      try {
+                        file.writeContent(outputStream);
+                      } finally {
+                        outputStream.close();
+                      }
                 ...
-              inputStream.close();
+              } finally {
+                inputStream.close();
+              }

174-176: Ensure temp ZIP is always cleaned up via finally.

If extraction throws, _cleanupTempFile() may not run. Wrap extraction phase in try/finally.

Apply this diff:

-          try {
+          try {
             if (!await _tempFile!.exists()) {
               throw Exception('קובץ הספרייה הזמני לא נמצא');
             }
             ...
-            await _cleanupTempFile();
-
-            emit(EmptyLibraryDownloaded());
-          } catch (e) {
+            emit(EmptyLibraryDownloaded());
+          } catch (e) {
             emit(EmptyLibraryError(errorMessage: 'שגיאה בחילוץ: $e'));
+          } finally {
+            await _cleanupTempFile();
           }

Also applies to: 195-207, 283-288

♻️ Duplicate comments (3)
lib/navigation/calendar_cubit.dart (2)

891-894: Potential null pointer dereference in _calculateChatzosLayla.

The function assumes getChatzos() returns a non-null value, but the return type indicates it could be null.


897-901: Potential null pointer dereference in _calculateSunsetRT.

Similar to the previous issue, getSunset() could return null.

.github/workflows/flutter.yml (1)

13-16: Gate releases to main/dev only (feature-branch pushes will publish releases).

These extra push branches will trigger the create_release job for feature branches. Remove them or guard the job by branch. This was flagged earlier and is still applicable.

Option A — restrict on.push:

   push: 
     branches:
       - main
       - dev
       - dev_dev2
-      - n-search2
-      - ns2new
-      - n-search2.1

Option B — gate the job (recommended even if Option A is applied):

 jobs:
   create_release:
-    if: github.event_name == 'push'
+    if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')
🧹 Nitpick comments (27)
.github/workflows/release-webhook.yml (4)

1-1: Set least‑privilege permissions.

Explicitly scope the GITHUB_TOKEN to read‑only for contents.

 name: Send Release Webhook
+permissions:
+  contents: read

10-12: Add a timeout to avoid hung runs.

Prevent indefinite executions if the script blocks on network I/O.

   send_webhook:
     runs-on: ubuntu-latest
+    timeout-minutes: 10

15-21: Pin GitHub Actions to immutable SHAs.

actions/checkout@v4 and actions/setup-python@v5 should be pinned to commit SHAs to mitigate supply‑chain risk. Keep the tag as a comment for readability.

Would you like me to propose the current SHAs for these actions?


26-34: Coalesce env vars for both triggers.

Make the env work for release and workflow_dispatch runs.

       - name: Run webhook script
         env:
-          RELEASE_TAG: ${{ github.event.release.tag_name }}
-          RELEASE_NAME: ${{ github.event.release.name }}
-          RELEASE_BODY: ${{ github.event.release.body }}
-          RELEASE_URL: ${{ github.event.release.html_url }}
+          RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
+          RELEASE_NAME: ${{ github.event.release.name || inputs.release_name }}
+          RELEASE_BODY: ${{ github.event.release.body || inputs.release_body }}
+          RELEASE_URL: ${{ github.event.release.html_url || inputs.release_url }}
           USER_NAME: ${{ secrets.USER_NAME }}
           PASSWORD: ${{ secrets.PASSWORD }}
           TOKEN_YEMOT: ${{ secrets.TOKEN_YEMOT }}
         run: python webhooks/main.py
lib/bookmarks/bookmark_screen.dart (8)

22-47: Tighten lifecycle: normalize query and guard focus.

  • Normalize query once and avoid redundant rebuilds.
  • Guard requestFocus with mounted to prevent edge-case callbacks after dispose.

Apply this diff:

   @override
   void initState() {
     super.initState();
-    _searchController.addListener(() {
-      setState(() {
-        _searchQuery = _searchController.text;
-      });
-    });
+    _searchController.addListener(() {
+      final q = _searchController.text.trim().toLowerCase();
+      if (q != _searchQuery) {
+        setState(() => _searchQuery = q);
+      }
+    });
 
     // Auto-focus the search field when the screen opens
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      _searchFocusNode.requestFocus();
+      if (!mounted) return;
+      _searchFocusNode.requestFocus();
     });
   }

74-77: Consider showing the search UI even when there are no bookmarks.

Helps users understand the new search affordance and prevents UI “jumping” when the first bookmark is added.


78-83: Avoid O(n^2) index lookups; precompute filtered entries with original indices.

Current use of indexOf per row is quadratic and can misfire if the list updates mid‑interaction.

Apply this diff:

-        // Filter bookmarks based on search query
-        final filteredBookmarks = _searchQuery.isEmpty
-            ? state.bookmarks
-            : state.bookmarks.where((bookmark) =>
-                bookmark.ref.toLowerCase().contains(_searchQuery.toLowerCase())).toList();
+        // Filter with original indices (prevents O(n^2) indexOf in the builder)
+        final filteredEntries = _searchQuery.isEmpty
+            ? state.bookmarks.asMap().entries.toList()
+            : state.bookmarks
+                .asMap()
+                .entries
+                .where((e) => e.value.ref.toLowerCase().contains(_searchQuery))
+                .toList();

84-111: Search field UX: add RTL alignment and “search” IME action.

Improves Hebrew input and accessibility.

Apply this diff:

               child: TextField(
                 controller: _searchController,
                 focusNode: _searchFocusNode,
+                textAlign: TextAlign.right,
+                textDirection: TextDirection.rtl,
+                textInputAction: TextInputAction.search,
                 decoration: InputDecoration(
                   hintText: 'חפש בסימניות...',
                   prefixIcon: const Icon(Icons.search),

95-102: Remove redundant setState; controller listener already updates state.

Prevents double rebuilds on clear.

Apply this diff:

-                          onPressed: () {
-                            _searchController.clear();
-                            setState(() {
-                              _searchQuery = '';
-                            });
-                          },
+                          onPressed: () => _searchController.clear(),

112-149: Use precomputed entries; add safety check and stable keys.

  • Switch to filteredEntries from above.
  • Guard against stale indices.
  • Provide a stable Key per row to help ListView diffing.

Apply this diff:

-              child: filteredBookmarks.isEmpty
+              child: filteredEntries.isEmpty
                   ? const Center(child: Text('לא נמצאו תוצאות'))
                   : ListView.builder(
-                itemCount: filteredBookmarks.length,
+                itemCount: filteredEntries.length,
                 itemBuilder: (context, index) {
-                  final bookmark = filteredBookmarks[index];
-                  final originalIndex = state.bookmarks.indexOf(bookmark);
+                  final entry = filteredEntries[index];
+                  final originalIndex = entry.key;
+                  final bookmark = entry.value;
                   return ListTile(
+                    key: ValueKey('${bookmark.ref}|${bookmark.index}'),
                     selected: false,
                     leading: bookmark.book is PdfBook
                         ? const Icon(Icons.picture_as_pdf)
                         : null,
                     title: Text(bookmark.ref),
                     onTap: () => _openBook(
                         context,
                         bookmark.book,
                         bookmark.index,
                         bookmark.commentatorsToShow),
                     trailing: IconButton(
-                      icon: const Icon(
-                        Icons.delete_forever,
-                      ),
+                      icon: const Icon(Icons.delete_forever),
                       onPressed: () {
-                        context
-                            .read<BookmarkBloc>()
-                            .removeBookmark(originalIndex);
+                        if (originalIndex < 0 || originalIndex >= state.bookmarks.length) return;
+                        context.read<BookmarkBloc>().removeBookmark(originalIndex);

137-139: Prefer deleting by ID/unique key instead of index.

Index-based deletes are brittle under concurrent updates/reordering. If Bookmark exposes an id/historyKey, emit a removeById event instead.

Can you confirm whether Bookmark has a unique identifier (e.g., id or historyKey) we can route through the BLoC? I can draft the event/state changes if so.


150-166: Tiny cleanup and UX consideration.

  • Remove redundant const in Duration.
  • Consider a confirmation dialog before mass deletion.

Apply this diff:

                   const SnackBar(
                     content: Text('כל הסימניות נמחקו'),
-                    duration: const Duration(milliseconds: 350),
+                    duration: Duration(milliseconds: 350),
                   ),
lib/navigation/calendar_cubit.dart (1)

1044-1044: Use const Duration for better performance.

Duration constructors can be made constant for compile-time optimization.

Apply this diff to improve performance:

-  return sunset.subtract(Duration(minutes: minutesBefore));
+  return sunset.subtract(Duration(minutes: minutesBefore));
-  return sunset.add(const Duration(minutes: 34));
+  return sunset.add(const Duration(minutes: 34));
-  return sunset.add(const Duration(minutes: 38));
+  return sunset.add(const Duration(minutes: 38));

Note: Line 1044 cannot use const because minutesBefore is a variable, but lines 1052 and 1060 are already using const correctly.

Also applies to: 1052-1052, 1060-1060

lib/history/history_screen.dart (6)

160-164: Initialize the tab before attaching it to TabsBloc to avoid blank/flicker races

Adding the tab before setting its query/options can briefly render an empty tab. Initialize first, then add the tab, then dispatch the search.

Apply:

-                        final tabsBloc = context.read<TabsBloc>();
-                        // Always create a new search tab instead of reusing existing one
-                        final searchTab = SearchingTab('חיפוש', null);
-                        tabsBloc.add(AddTab(searchTab));
+                        final tabsBloc = context.read<TabsBloc>();
+                        // Always create a new search tab instead of reusing existing one
+                        final searchTab = SearchingTab('חיפוש', null);
@@
-                        // Trigger search
-                        searchTab.searchBloc.add(UpdateSearchQuery(
+                        // Attach after initialization to avoid flicker
+                        tabsBloc.add(AddTab(searchTab));
+                        // Trigger search
+                        searchTab.searchBloc.add(UpdateSearchQuery(

Also applies to: 177-186


167-176: Avoid untyped {} defaults in addAll — risk of type inference to Map<dynamic,dynamic>

Use null checks with non-null assertion to keep collection types intact.

Apply:

-                        searchTab.searchOptions.clear();
-                        searchTab.searchOptions
-                            .addAll(historyItem.searchOptions ?? {});
+                        searchTab.searchOptions..clear();
+                        if (historyItem.searchOptions != null) {
+                          searchTab.searchOptions.addAll(historyItem.searchOptions!);
+                        }
@@
-                        searchTab.alternativeWords.clear();
-                        searchTab.alternativeWords
-                            .addAll(historyItem.alternativeWords ?? {});
+                        searchTab.alternativeWords..clear();
+                        if (historyItem.alternativeWords != null) {
+                          searchTab.alternativeWords.addAll(historyItem.alternativeWords!);
+                        }
@@
-                        searchTab.spacingValues.clear();
-                        searchTab.spacingValues
-                            .addAll(historyItem.spacingValues ?? {});
+                        searchTab.spacingValues..clear();
+                        if (historyItem.spacingValues != null) {
+                          searchTab.spacingValues.addAll(historyItem.spacingValues!);
+                        }

41-43: Guard autofocus with mounted inside post-frame callback

Prevents requesting focus after disposal during quick route changes.

Apply:

-    WidgetsBinding.instance.addPostFrameCallback((_) {
-      _searchFocusNode.requestFocus();
-    });
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      if (!mounted) return;
+      _searchFocusNode.requestFocus();
+    });

131-136: Remove redundant setState — listener already updates _searchQuery on clear

Avoids double rebuild.

Apply:

-                          onPressed: () {
-                            _searchController.clear();
-                            setState(() {
-                              _searchQuery = '';
-                            });
-                          },
+                          onPressed: () {
+                            _searchController.clear();
+                          },

59-61: DRY: extract openLeftPane expression once

Improves readability; avoids evaluating settings twice.

Apply:

-    final tab = book is PdfBook
+    final openLeftPane =
+        (Settings.getValue<bool>('key-pin-sidebar') ?? false) ||
+        (Settings.getValue<bool>('key-default-sidebar-open') ?? false);
+    final tab = book is PdfBook
         ? PdfBookTab(
             book: book,
             pageNumber: index,
-            openLeftPane: (Settings.getValue<bool>('key-pin-sidebar') ??
-                    false) ||
-                (Settings.getValue<bool>('key-default-sidebar-open') ?? false),
+            openLeftPane: openLeftPane,
           )
         : TextBookTab(
             book: book as TextBook,
             index: index,
             commentators: commentators,
-            openLeftPane: (Settings.getValue<bool>('key-pin-sidebar') ??
-                    false) ||
-                (Settings.getValue<bool>('key-default-sidebar-open') ?? false),
+            openLeftPane: openLeftPane,
           );

Also applies to: 67-70


123-145: UX: add keyboard actions for the search field

Optional: add textInputAction: TextInputAction.search and onSubmitted: (_) => _searchFocusNode.unfocus() to improve ergonomics.

installer/otzaria_full.iss (1)

3308-3308: Potential data loss: deleting default.isar on every install.
If default.isar contains user data, this wipes it during full installs. If the intent is only to remove a stale file, restrict to files to avoid directory semantics.

Consider:

-Type: filesandordirs; Name: "{app}\default.isar";
+Type: files; Name: "{app}\default.isar";

Confirm whether this delete is truly desired on upgrade paths for the “full” build.

.github/workflows/flutter.yml (3)

34-66: Harden ISCC.exe discovery (handle per‑user installs).

Winget can install Inno Setup under LOCALAPPDATA; the current search only checks Program Files. Add a LOCALAPPDATA fallback before failing.

-  $iscc = (Get-ChildItem "C:\Program Files*\Inno Setup*\ISCC.exe" -Recurse -ErrorAction SilentlyContinue |
-           Select-Object -First 1 -ExpandProperty FullName)
-  if (-not $iscc) { throw "ISCC.exe not found after installation" }
+  $candidates = @(
+    "C:\Program Files*\Inno Setup*\ISCC.exe",
+    "$env:LOCALAPPDATA\Programs\Inno Setup*\ISCC.exe"
+  )
+  $iscc = $null
+  foreach ($pat in $candidates) {
+    $hit = Get-ChildItem $pat -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
+    if ($hit) { $iscc = $hit; break }
+  }
+  if (-not $iscc) { throw "ISCC.exe not found after installation (checked Program Files and LOCALAPPDATA)" }

82-85: Make MSIX build non‑blocking and align upload behavior.

msix:create can fail if tooling/cert isn’t present; let the pipeline proceed and have upload ignore missing files.

Within this step:

-      - name: Build MSIX package
-        shell: pwsh
-        run: |
-          dart run msix:create --install-certificate false
+      - name: Build MSIX package
+        shell: pwsh
+        continue-on-error: true
+        run: |
+          dart run msix:create --install-certificate false

And in “Upload Windows MSIX” (outside this hunk):

-      - name: Upload Windows MSIX
+      - name: Upload Windows MSIX
         uses: actions/upload-artifact@v4
         with:
           name: otzaria.msix
-          path: build/windows/x64/runner/release/*.msix
+          path: build/windows/x64/runner/release/*.msix
+          if-no-files-found: ignore

372-377: Standardize PR tag prefix (consistency with release tags).

PR prerelease tags drop the leading “v”. Consider using v$VERSION-pr-… or pr/v$VERSION-… for consistent sorting and tooling that expects a v‑prefixed semver.

-          echo "tag=$VERSION-pr-${{ github.event.number }}-${{ github.run_number }}" >> $GITHUB_OUTPUT
+          echo "tag=v$VERSION-pr-${{ github.event.number }}-${{ github.run_number }}" >> $GITHUB_OUTPUT
# or
+# echo "tag=pr/v$VERSION-${{ github.event.number }}-${{ github.run_number }}" >> $GITHUB_OUTPUT
lib/empty_library/bloc/empty_library_bloc.dart (4)

134-143: Use a system temp directory for transient downloads, not Documents.

Prefer getTemporaryDirectory() for _tempFile to avoid cluttering user documents and to get auto‑cleanup on some platforms.

Apply this diff:

-      final tempDir = await getApplicationDocumentsDirectory();
+      final tempDir = await getTemporaryDirectory();

145-158: Nit: variable name implies bytes but holds MB.

_lastDownloadedBytes tracks MB. Rename to _lastDownloadedMB to avoid confusion.


171-191: Optional: guard by expected content type and a max size.

Before starting extraction, validate response.headers['content-type'] is application/zip or application/octet-stream, and fail if contentLength exceeds a sane cap to avoid zip bombs.


321-331: State naming: emitting “Downloaded” after directory pick is misleading.

Consider a dedicated DirectorySelected state to avoid UI reacting as if content was actually downloaded.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b05d83e and 0bd6cd2.

📒 Files selected for processing (10)
  • .github/workflows/flutter.yml (3 hunks)
  • .github/workflows/release-webhook.yml (1 hunks)
  • .gitignore (1 hunks)
  • installer/otzaria.iss (2 hunks)
  • installer/otzaria_full.iss (2 hunks)
  • lib/bookmarks/bookmark_screen.dart (2 hunks)
  • lib/empty_library/bloc/empty_library_bloc.dart (1 hunks)
  • lib/history/history_screen.dart (3 hunks)
  • lib/navigation/calendar_cubit.dart (1 hunks)
  • lib/navigation/calendar_widget.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • .gitignore
  • installer/otzaria.iss
  • lib/navigation/calendar_widget.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build_macos
  • GitHub Check: build_android
  • GitHub Check: build_linux
  • GitHub Check: build_windows
🔇 Additional comments (12)
.github/workflows/release-webhook.yml (2)

25-35: LGTM overall.

The workflow wiring is clear and minimal. With the above adjustments, it’ll be robust for both release and manual runs.


26-35: Secrets hygiene — confirm secrets exist & are not logged.

webhooks/main.py is present and reads RELEASE_TAG/RELEASE_NAME/RELEASE_BODY/RELEASE_URL and USER_NAME/PASSWORD/TOKEN_YEMOT (envs on lines ~17–24) and instantiates MitmachimClient with username/password (line ~38). Ensure USER_NAME, PASSWORD, TOKEN_YEMOT are set as repo/org secrets and that webhooks/main.py never logs/prints these values (search for print()/logging.*/f-strings containing them). Verify the workflow path (webhooks/main.py) is correct.

lib/bookmarks/bookmark_screen.dart (1)

15-20: Stateful migration looks good; no public API break.

Constructor remains const; imports and call sites should be unaffected.

Please confirm that any tests relying on StatelessWidget-specific behavior (e.g., const subtree optimizations) still pass.

lib/history/history_screen.dart (3)

84-86: Verify: Why is .docx handled under PdfBook?

If DOCX entries aren’t modeled as PdfBook, this branch is unreachable or misclassified. Consider basing the icon purely on file extension or actual type.


185-191: Confirm pop-after-navigation behavior

NavigateToScreen(Screen.search) via bloc plus Navigator.pop() can unintentionally pop a page if this isn’t inside a dialog. Please confirm this screen is only ever shown modally when canPop == true.


159-193: Sanity-check passed — SearchingTab constructor/fields align with history model.
SearchingTab accepts a nullable searchText and exposes queryController, searchBloc, searchOptions (Map<String, Map<String, bool>>), alternativeWords (Map<int, List>) and spacingValues (Map<String, String>); the Bookmark/history item defines matching nullable fields. Verified in lib/tabs/models/searching_tab.dart, lib/bookmarks/models/bookmark.dart and lib/history/bloc/history_bloc.dart.

installer/otzaria_full.iss (2)

5-5: Version bump verified — consistent across installers and CI artifacts.
installer/otzaria.iss:5 and installer/otzaria_full.iss:5 define MyAppVersion "0.9.53"; both OutputBaseFilename lines use otzaria-{#MyAppVersion}-windows(-full) (line 27); version.json and pubspec.yaml contain 0.9.53; update_version.ps1 updates both; .github/workflows/flutter.yml uses installer/otzaria-*-windows.exe (wildcard).


3325-3328: Incorrect — Excludes uses commas, not semicolons. Inno Setup's Excludes parameter expects comma-separated patterns; the current entries already use commas, so switching to semicolons would be incorrect. (documentation.help)

Files to ignore: installer/otzaria_full.iss (lines 3325–3327) and installer/otzaria.iss (line 52) — Excludes currently: ".msix,.msixbundle,.appx,.appxbundle,*.appinstaller".

If you need stricter filtering, add path-aware masks (for example "\release*" or "\Release*") or additional patterns, but do not replace the commas with semicolons.

Likely an incorrect or invalid review comment.

.github/workflows/flutter.yml (4)

23-23: Good pin: windows-2022 is stable vs windows-latest drift.


26-27: Checkout v4 is current and fine.


68-68: pwsh for Windows build = consistent with later steps.


87-91: ISCC via explicit path is correct; keep quoting.

Comment on lines +3 to +8
on:
release:
types: [published]

workflow_dispatch: # מאפשר הפעלה ידנית

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Manual trigger has no release context — envs will be empty.

When run via workflow_dispatch, github.event.release.* is undefined, so RELEASE_* envs become empty and the script may misbehave. Add inputs and coalesce with release event fields.

Apply this diff:

 on:
   release:
     types: [published]

-  workflow_dispatch:  # מאפשר הפעלה ידנית
+  workflow_dispatch:  # מאפשר הפעלה ידנית
+    inputs:
+      release_tag:
+        description: "Tag (e.g., v1.2.3)"
+        required: true
+      release_name:
+        description: "Release name"
+        required: false
+      release_body:
+        description: "Release notes (optional)"
+        required: false
+      release_url:
+        description: "Release URL (optional)"
+        required: false

And coalesce env vars (see lines 26-34 diff in a separate comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
release:
types: [published]
workflow_dispatch: # מאפשר הפעלה ידנית
on:
release:
types: [published]
workflow_dispatch: # מאפשר הפעלה ידנית
inputs:
release_tag:
description: "Tag (e.g., v1.2.3)"
required: true
release_name:
description: "Release name"
required: false
release_body:
description: "Release notes (optional)"
required: false
release_url:
description: "Release URL (optional)"
required: false
🤖 Prompt for AI Agents
In .github/workflows/release-webhook.yml around lines 3 to 8, the
workflow_dispatch manual trigger runs without release context so
github.event.release.* is undefined and RELEASE_* envs become empty; add inputs
to the workflow_dispatch for the needed release fields (tag_name, name, body,
draft, prerelease, created_at, published_at, author_login, etc.) and then
coalesce environment variables (lines ~26-34) to use the provided inputs first
and fall back to github.event.release.* when inputs are empty (e.g., set
RELEASE_TAG: ${{ inputs.tag_name || github.event.release.tag_name }}), ensuring
scripts receive sensible values when manually triggered.

Comment on lines +22 to +24
- name: Install dependencies
run: pip install bs4 requests pyluach

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unpinned Python deps in CI.

Installing bs4/requests/pyluach without pinning is a supply‑chain risk and harms reproducibility. Prefer a hashed requirements file.

Suggested change (you’ll need to add webhooks/requirements.txt with pinned versions and --hash entries):

-      - name: Install dependencies
-        run: pip install bs4 requests pyluach
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install --require-hashes -r webhooks/requirements.txt

Minor: install beautifulsoup4 instead of the bs4 shim for clarity.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
.github/workflows/release-webhook.yml lines 22-24: the workflow installs
unpinned Python packages which is a supply-chain and reproducibility risk;
replace the direct pip install with pip install -r webhooks/requirements.txt
--require-hashes and create webhooks/requirements.txt containing pinned versions
with their corresponding --hash entries (use beautifulsoup4 instead of the bs4
shim and include requests and pyluach) so CI installs deterministic, verified
packages.

'GET',
Uri.parse(
'https://github.com/Sivan22/otzaria-library/releases/download/latest/otzaria_latest.zip'),
'https://github.com/zevisvei/otzaria-library/releases/download/latest/otzaria_latest.zip'),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Download origin changed to a different GitHub account; pin and verify artifacts instead of using “latest”.

Switching to github.com/zevisvei/... is a supply‑chain risk. Avoid mutable latest URLs; pin to a specific tag/commit and verify checksums before extraction. Add fallback/mirror or a server‑side redirect you control.

Would you confirm ownership/transfer of the new repo and whether a signed checksum (e.g., SHA‑256) is published per release so we can validate the ZIP before extraction?


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching repository for 'otzaria_latest.zip', 'zevisvei' and related strings..."
rg -n --hidden -S "otzaria_latest.zip|zevisvei|otzaria-library" -C2 || true

echo
echo "Showing lib/empty_library/bloc/empty_library_bloc.dart lines 150-170 if present..."
if [ -f "lib/empty_library/bloc/empty_library_bloc.dart" ]; then
  nl -ba lib/empty_library/bloc/empty_library_bloc.dart | sed -n '150,170p'
else
  echo "File not found: lib/empty_library/bloc/empty_library_bloc.dart"
  echo "Searching for any file that contains 'otzaria' under lib/..."
  rg -n --hidden -S "otzaria" lib -C2 || true
  echo "Listing lib/empty_library directory (if exists)..."
  ls -la lib/empty_library || true
fi

Length of output: 3199


Stop downloading 'latest' from an external GitHub account — pin releases and verify checksums.

Found a mutable release URL pointing to a different owner (supply‑chain risk). Pin to a specific tag/commit, validate the ZIP (SHA‑256 or GPG-signed checksum) before extraction, and add a server-controlled mirror/fallback.

Confirm ownership/transfer of zevisvei/otzaria-library and whether signed checksums (SHA‑256 or GPG) are published per release so we can validate before extraction.

🤖 Prompt for AI Agents
lib/empty_library/bloc/empty_library_bloc.dart line 163: the code downloads a
mutable "latest" ZIP from zevisvei's GitHub which is a supply‑chain risk; change
the URL to a pinned release asset (use a specific tag or commit SHA in the
release URL), add code to fetch and verify a published checksum (preferably
SHA‑256 or GPG signature) before extracting, and add a configurable
server-controlled mirror/fallback URL to use if verification fails; also update
lib/library/view/library_browser.dart and README links to use the same pinned
owner/repo/branch values or your verified mirror and verify ownership/transfer
of zevisvei/otzaria-library and that signed checksums are published for the
pinned release.

Comment on lines +112 to +117
// Filter history based on search query
final filteredHistory = _searchQuery.isEmpty
? state.history
: state.history.where((item) =>
item.ref.toLowerCase().contains(_searchQuery.toLowerCase())).toList();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Delete-by-index may remove the wrong item when duplicates exist; avoid O(n²) rendering

state.history.indexOf(historyItem) picks the first equal item; duplicates will delete the wrong row, and doing this per row is O(n²).

Refactor to carry original indices via asMap():

-        // Filter history based on search query
-        final filteredHistory = _searchQuery.isEmpty
-            ? state.history
-            : state.history.where((item) =>
-                item.ref.toLowerCase().contains(_searchQuery.toLowerCase())).toList();
+        // Filter history while preserving original indices
+        final q = _searchQuery.trim().toLowerCase();
+        final filteredEntries = (_searchQuery.isEmpty
+                ? state.history.asMap().entries
+                : state.history
+                    .asMap()
+                    .entries
+                    .where((e) => e.value.ref.toLowerCase().contains(q)))
+            .toList();
@@
-              child: filteredHistory.isEmpty
+              child: filteredEntries.isEmpty
@@
-                itemCount: filteredHistory.length,
+                itemCount: filteredEntries.length,
@@
-                  final historyItem = filteredHistory[index];
-                  final originalIndex = state.history.indexOf(historyItem);
+                  final entry = filteredEntries[index];
+                  final historyItem = entry.value;
+                  final originalIndex = entry.key;

Also applies to: 147-154

🤖 Prompt for AI Agents
In lib/history/history_screen.dart around lines 112-117 (also apply same fix to
147-154): the filtered list and subsequent delete action rely on
state.history.indexOf(historyItem), which returns the first equal item (wrong
with duplicates) and causes O(n²) behavior; instead, produce the filtered list
with original indices (use state.history.asMap().entries and filter by value to
build a List of MapEntry<int, HistoryItem> or a tuple of (index,item)), use the
original index when rendering rows and when dispatching delete actions, and
avoid calling indexOf per row so deletion targets the correct item and rendering
is O(n).

Comment on lines +159 to +167
if (historyItem.isSearch) {
final tabsBloc = context.read<TabsBloc>();
// Always create a new search tab instead of reusing existing one
final searchTab = SearchingTab('חיפוש', null);
tabsBloc.add(AddTab(searchTab));

// Restore search query and options
searchTab.queryController.text = historyItem.book.title;
searchTab.searchOptions.clear();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix crash and wrong query restoration for search history items

Accessing historyItem.book.title when isSearch == true can be null and crash; also it restores the book title instead of the saved search query. Use the history entry’s textual ref for the search box.

Apply:

-                        // Restore search query and options
-                        searchTab.queryController.text = historyItem.book.title;
+                        // Restore search query and options
+                        // For search history entries, use the saved ref/query text
+                        searchTab.queryController.text = historyItem.ref;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (historyItem.isSearch) {
final tabsBloc = context.read<TabsBloc>();
// Always create a new search tab instead of reusing existing one
final searchTab = SearchingTab('חיפוש', null);
tabsBloc.add(AddTab(searchTab));
// Restore search query and options
searchTab.queryController.text = historyItem.book.title;
searchTab.searchOptions.clear();
if (historyItem.isSearch) {
final tabsBloc = context.read<TabsBloc>();
// Always create a new search tab instead of reusing existing one
final searchTab = SearchingTab('חיפוש', null);
tabsBloc.add(AddTab(searchTab));
// Restore search query and options
// For search history entries, use the saved ref/query text
searchTab.queryController.text = historyItem.ref;
searchTab.searchOptions.clear();
🤖 Prompt for AI Agents
In lib/history/history_screen.dart around lines 159 to 167, the code reads
historyItem.book.title for search history items which can be null and also
restores the book title instead of the saved search query; replace that access
with the history entry’s textual reference (e.g. historyItem.textualRef) and
guard against null by setting searchTab.queryController.text =
historyItem.textualRef ?? '' so the query restores correctly and no null access
occurs; leave the search options restoration as-is or map from historyItem if a
saved options field exists.

@Y-PLONI Y-PLONI closed this Dec 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants