Skip to content

No account#17

Merged
swstl merged 21 commits intomainfrom
no-account
Feb 3, 2026
Merged

No account#17
swstl merged 21 commits intomainfrom
no-account

Conversation

@swstl
Copy link
Copy Markdown
Owner

@swstl swstl commented Feb 3, 2026

Added option to use this TUI without logging in.
Saves watch history and changes to a local sqlite database instead of just a file.
Syncs local changes to MyAnimeList when/if logging in after changing anything locally.
Change name to mal-tui cus mal-cli was taken.
Small fixes
Added homepage

Summary by CodeRabbit

  • New Features

    • Local SQLite storage for offline use and watch-history persistence.
    • Interactive sync/unsync popup to manage local ↔ remote anime sync.
    • Terminal-style landing page and profile-picture (pfp) image endpoint.
    • Health endpoint and NSFW toggle in config.
  • Improvements

    • More robust background synchronization and startup fetching.
    • Docker build updated to include FFmpeg for media processing.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 3, 2026

Warning

Rate limit exceeded

@swstl has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a SQLite-backed ORM and proc-macro for Entry, integrates a local DatabaseManager into app and MalClient for offline sync, implements extensive background synchronization UI (SyncPopup) and flows, refactors network auth to an Identifier, and adds server endpoints (/health, /pfp) plus a static index page.

Changes

Cohort / File(s) Summary
Workspace & database crates
Cargo.toml, deps/database/Cargo.toml, deps/database/src/lib.rs
Bump root version; add database workspace crate and re-exports.
Database core
deps/database/core/Cargo.toml, deps/database/core/src/lib.rs
New core crate: DatabaseManager (Arc<Mutex>) and Entryable trait with CRUD, upsert, clear, and table/row mapping.
Entry proc-macro
deps/database/entry/Cargo.toml, deps/database/entry/src/lib.rs
New entry proc-macro crate adding #[derive(Entry)] generation: schema, primary key/foreign key handling, bind/from_row, JSON serialization for complex types.
Server & static assets
server/Dockerfile, server/compose.yaml, server/src/main.rs, server/static/index.html
Docker base image → Ubuntu 24.04, add ffmpeg and asset copying; compose uses local build; server adds /health and /pfp endpoints (ffmpeg frame extraction), dynamic index.html serving; new static landing page.
App wiring & state
src/app.rs
Inject DatabaseManager into ExtraInfo (local_db); add Action::SyncAnimes and Event::SyncStatus; persist/upsert Anime and WatchHistory on logging; integrate sync flow.
Network & auth refactor
src/mal/network.rs, src/mal/mod.rs
Introduce Identifier (auth_token + client_id) and to_headers(); refactor fetch/update APIs to accept Identifier; extend Update trait with to_offline_response/pre_update/update_local; MalClient now stores local_db and caches client_id.
Models & storage
src/mal/models/anime.rs, src/mal/models/mod.rs, src/mal/models/user.rs
Derive Entry for Anime (primary key), add WatchHistory model, add fields to User, add offline/update helpers and Storable/Update integrations.
Screens & background sync
src/screens/mod.rs, src/screens/launch.rs, src/screens/list.rs, src/screens/overview.rs, src/screens/login.rs, src/screens/profile.rs, src/screens/search.rs, src/screens/seasons.rs
Add Screen::background hook and auth gating macro; LaunchScreen, List, Overview integrate local DB background fetch/sync flows; batch-size changes and UI/logic refinements across screens; Login & Profile UI adjustments.
Sync UI widget
src/screens/widgets/mod.rs, src/screens/widgets/sync.rs, src/screens/widgets/popup.rs, src/screens/widgets/animebox.rs
Export new sync module; new SyncPopup UI component with per-item/batch sync/delete actions, progress summary, and keyboard/mouse handling; refactor anime box rendering and popup styling.
Input & utils
src/handlers/input_handler.rs, src/utils/functionStreaming.rs, src/utils/stringManipulation.rs
Removed Ctrl-C quit behavior; renamed max_of_batchesmax_nr_of_batches and swapped change_batch_size_at param order; added format_date_short.
Player & other minor API changes
src/player/mod.rs, src/screens/widgets/*
Rename PlayResult.completed → is_completed; adjust mpv command/template handling and minor UI rendering changes.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI (Screen)
    participant App as App
    participant MalClient as MalClient
    participant LocalDB as DatabaseManager (SQLite)
    participant RemoteAPI as MAL Remote API

    UI->>App: Trigger sync (user or background)
    App->>MalClient: Request local anime list
    MalClient->>LocalDB: Query stored Anime
    LocalDB-->>MalClient: Return local anime list

    par Fetch remote
        MalClient->>RemoteAPI: Fetch remote anime batches (Identifier)
        RemoteAPI-->>MalClient: Return batch
    end

    MalClient->>MalClient: Compare local vs remote, build conflicts
    MalClient->>App: Emit per-anime sync updates
    App->>UI: Render/Update SyncPopup

    UI->>App: User chooses action (sync/delete)
    App->>MalClient: Execute update for item(s)

    alt Logged in
        MalClient->>RemoteAPI: Send update request
        RemoteAPI-->>MalClient: Confirm
    end

    MalClient->>LocalDB: Upsert/delete local records
    LocalDB-->>MalClient: Confirm
    MalClient->>App: Emit SyncStatus(success)
    App->>UI: Update UI / close popup
Loading
sequenceDiagram
    participant Client as Client App
    participant Server as Server
    participant FFmpeg as FFmpeg
    participant FS as Filesystem

    Client->>Server: GET /pfp
    Server->>Server: choose random timestamp
    Server->>FFmpeg: run ffmpeg to extract frame (350x225) at timestamp
    FFmpeg-->>FS: write temporary PNG
    FS-->>Server: read PNG bytes
    Server-->>Client: Respond image/png
    Server->>FS: remove temp file
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

  • Config #15 — touches Config and Config::global usage; related to recent config/NSFW gating and data_dir references introduced here.

Poem

🐰 In burrows of bytes I nibble and sew,

Tables and rows in a cozy SQLite row,
Sync bells are ringing, conflicts take flight,
I hop through the popup and set things aright,
A carrot of ffmpeg pops frames into night. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'No account' accurately reflects a core feature introduced in this PR: enabling offline/local-only usage without requiring user login.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch no-account

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@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: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
src/screens/login.rs (1)

175-183: ⚠️ Potential issue | 🟠 Major

Document MSRV requirement or use alternative syntax for the let-chains pattern.

The if let ... && let ... syntax (let-chains) requires Rust 1.76.0 or higher. The project has no documented MSRV, which means this code introduces an undocumented dependency on a specific Rust version. Either:

  1. Add rust-version = "1.76" to Cargo.toml to document the MSRV requirement, or
  2. Refactor using nested if let statements to maintain compatibility with older Rust versions
🤖 Fix all issues with AI agents
In `@deps/database/core/Cargo.toml`:
- Around line 1-4: The crate named "core" in Cargo.toml shadows Rust's sysroot
core; rename the package from "core" to a non-conflicting name like "db_core" or
"core_lib" in the Cargo.toml package.name entry and update all workspace members
and dependent crates' Cargo.toml entries that list "core" as a dependency to use
the new name; also update any use/import paths or crate references (e.g., extern
crate core; or use core::...) inside source files to the new crate name where
they refer to your crate (but leave all references to the standard library's
::core unchanged).

In `@deps/database/core/src/lib.rs`:
- Around line 100-119: The update method currently interpolates the condition
string directly into the SQL causing SQL injection; change update<T:
Entryable>(&self, obj: T, condition: &str) to accept parameterized condition
plus its parameters (e.g., condition: &str, condition_params: impl
IntoIterator<Item=rusqlite::types::Value>) and do not concatenate the condition
into SQL raw — require the caller to use ? placeholders in condition and then
build the final query with placeholders only, collect the obj.bind_values() into
a values Vec and then extend that Vec with the condition_params, and finally
call connection.execute(&query, rusqlite::params_from_iter(combined_values)).
Reference symbols: update, Entryable::bind_values, table_name, set_clauses, and
rusqlite::params_from_iter.
- Around line 78-98: The upsert method currently ensures the table exists by
calling T::schema() and running "CREATE TABLE IF NOT EXISTS ..." but insert does
not, causing inconsistent behavior; fix by making behavior consistent: either
(A) add the same table-creation logic to insert (use T::table_name() and
T::schema() and run the CREATE TABLE IF NOT EXISTS before executing the INSERT),
or (B) extract the table-creation code into a helper (e.g.,
ensure_table_exists(&connection, &table_name, &schema) or
self.ensure_table<T>()) and call that helper from both upsert and insert,
ensuring both use T::table_name() and T::schema() to create the table if
missing.
- Around line 121-133: The delete method assumes the primary key column is "id"
which is brittle; add a method on the Entryable trait (for example
primary_key_name() or p_key_column()) that returns the column name for the PK,
implement it for types that already implement Entryable, and update the delete
function to use T::primary_key_name() together with obj.p_key() to build the
WHERE clause (referencing the delete method, Entryable trait, p_key(), and
table_name()); ensure the query properly escapes or formats the column name when
constructing the DELETE statement.
- Around line 40-57: The get method currently interpolates the condition string
directly into SQL (SQL injection risk) and has inconsistent indentation; change
the signature of get (pub fn get<T: Entryable>) to accept an optional
parameterized WHERE clause plus its bound parameters (e.g., Option<(&str, &[&dyn
ToSql])> or similar), build the SQL as "SELECT * FROM {table_name}" and append "
WHERE {clause}" only when the clause is provided, then pass the parameter slice
into connection.prepare/query_map (instead of embedding values into the string),
and tidy up the function indentation; reference the get function and
T::table_name() when making these changes.

In `@deps/database/entry/src/lib.rs`:
- Around line 311-316: The generated from_row code currently calls
serde_json::from_str(&row.get::<_, String>(`#index`)?).unwrap(), which can panic
on malformed JSON; replace the unwrap with propagation of a proper error by
mapping serde_json::Error into the function's error type and using ? (e.g., call
serde_json::from_str(...).map_err(|e| /* convert to rusqlite::Error or the
from_row Result's error */)?), so the from_row implementation returns an Err
instead of panicking when deserialization fails; update the generated template
around serde_json::from_str and row.get::<_, String>(`#index`) accordingly.
- Around line 207-221: The generated code in bind_values uses
serde_json::to_string(...).unwrap() for complex fields (occurrences of
serde_json::to_string in the quote! blocks), which can panic; change the
generator so bind_values propagates serialization errors instead of unwrapping:
update the bind_values signature to return Result<..., Error> (or
Result<Vec<rusqlite::types::Value>, rusqlite::Error/any appropriate error type)
and replace each .unwrap() with ? or map_err(...) to convert the
serde_json::Error into the chosen error type; ensure both branches that
currently produce
rusqlite::types::Value::from(serde_json::to_string(...).unwrap()) and the
.map(|v| serde_json::to_string(v).unwrap()) form use fallible serialization that
returns Err on failure so the caller can handle the error.

In `@server/src/main.rs`:
- Around line 285-317: The /pfp route handler currently panics on errors and has
fragile assumptions; replace the inline expect() usages (the ffmpeg
Command::new("ffmpeg") call and fs::read / fs::remove_file uses) with proper
error handling that returns an HTTP error response instead of crashing, verify
the source video exists via fs::metadata before running extraction, obtain
VIDEO_DURATION dynamically (e.g., via ffprobe) or fall back to a safe default,
use a collision-safe temp file creation strategy (e.g., tempfile::NamedTempFile
or a UUID) instead of rng.random::<u32>(), check Command::output() /
output.status.success() and log/return 500 on failure, read the temp file with
error handling and always attempt cleanup in a finally-style block to remove the
temp file (using fs::remove_file but ignore errors), and ensure all failures
produce a controlled rouille::Response with appropriate status instead of
calling expect().

In `@src/mal/models/mod.rs`:
- Around line 12-21: The DB ID fields use usize which rusqlite cannot
deserialize; change all ID field types from usize to i64 in the structs
referenced (WatchHistory, Anime, User) — specifically replace fields named id,
anime_id, user_id, etc., with i64 so the Entry-derived row conversion uses i64
and row.get::<_, i64>() succeeds; then update any code that assumes usize
(function params, returns, casts, comparisons) to convert from/to usize only
where necessary (e.g., when interfacing with non-DB APIs) to keep types
consistent with rusqlite.

In `@src/screens/search.rs`:
- Around line 291-298: The code uses "if let ... && ..." (let chains) in
expressions like the block calling search_input.handle_event(...) that sets
self.fetching and sends LocalEvent::Search via self.bg_sender, but your
Cargo.toml has an invalid edition "2024" and no toolchain guarantee: either
change the edition to a valid value (e.g., "2021") and ensure the
project/toolchain is Rust 1.76+ (update rust-toolchain or CI) so let_chains is
supported, or else refactor the three occurrences (the if-let around
search_input.handle_event, the similar checks that update self.fetching and use
bg_sender) into nested if let / if !text.is_empty() statements to avoid
let_chains entirely.

In `@src/screens/widgets/popup.rs`:
- Around line 125-131: The failure payload currently stores (index, String)
which mismatches the expected usize when later retrieved, so change the
.set("failure", ...) calls in the BackgroundUpdate creation to pass just the
index (usize) rather than a tuple; update the occurrence inside the match
handling update_user_list (the Err(e) branch that calls
BackgroundUpdate::new("popup").set("failure", ...)) and the second similar
occurrence later (lines ~178-184) so both failure branches send the same usize
type the consumer expects.

In `@src/screens/widgets/sync.rs`:
- Around line 338-342: The count for already_removed is using the wrong
predicate: in the closure passed to
self.finished_processing.iter().filter(|(sync, _)| *sync).count() you should
invert the boolean so it counts removed entries, i.e. use !*sync; update the
filter in the already_removed computation (referencing finished_processing and
already_removed) to use |(sync, _)| !*sync so it no longer duplicates the
already_synced logic.
🟡 Minor comments (11)
server/static/index.html-570-575 (1)

570-575: ⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" for links opened in a new tab.

Prevents tab‑nabbing and removes access to window.opener.

🔒 Suggested change
-    <a href="https://github.com/swstl/mal-cli" class="btn btn-primary" target="_blank">
+    <a href="https://github.com/swstl/mal-cli" class="btn btn-primary" target="_blank" rel="noopener noreferrer">
         View on GitHub
     </a>
-    <a href="https://github.com/swstl/mal-cli#installation" class="btn btn-secondary" target="_blank">
+    <a href="https://github.com/swstl/mal-cli#installation" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">
         Documentation
     </a>
src/utils/stringManipulation.rs-91-109 (1)

91-109: ⚠️ Potential issue | 🟡 Minor

Use NaiveDateTime for timezone-less timestamps.

DateTime::parse_from_str requires a timezone offset in the input and will fail on strings like 2025-07-06T15:08. Parse as NaiveDateTime instead.

Suggested fix
-use chrono::{DateTime, NaiveDate};
+use chrono::{DateTime, NaiveDate, NaiveDateTime};
@@
-    if let Ok(dt) = DateTime::parse_from_str(date_str, "%Y-%m-%dT%H:%M") {
+    if let Ok(dt) = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%dT%H:%M") {
         return dt.format("%b %d, %Y at %I:%M %p").to_string();
     }
deps/database/entry/src/lib.rs-128-130 (1)

128-130: ⚠️ Potential issue | 🟡 Minor

Potential truncation: casting to usize may lose precision.

The generated p_key() method casts the primary key field to usize. On 32-bit systems, this could truncate values if the primary key exceeds u32::MAX. Consider using u64 for the trait method or documenting this limitation.

deps/database/core/src/lib.rs-143-152 (1)

143-152: ⚠️ Potential issue | 🟡 Minor

Potential issue: PRAGMA changes affect all subsequent operations.

clear_force disables and re-enables foreign keys, but if an error occurs between disable and re-enable, foreign keys remain disabled for subsequent operations on this connection.

🛡️ Consider using a transaction or ensuring cleanup
     pub fn clear_force<T: Entryable>(&self) -> Result<(), Error> {
         let connection = self.connection.lock().unwrap();
         let table_name = T::table_name();
         connection.execute("PRAGMA foreign_keys = OFF", [])?;
         let result = connection.execute(&format!("DELETE FROM {}", table_name), []);
-        connection.execute("PRAGMA foreign_keys = ON", [])?;
-        result?;
+        // Always re-enable foreign keys, even on error
+        let pragma_result = connection.execute("PRAGMA foreign_keys = ON", []);
+        result?;
+        pragma_result?;
         Ok(())
     }
src/screens/widgets/popup.rs-669-673 (1)

669-673: ⚠️ Potential issue | 🟡 Minor

Guard against underflow in narrow terminals.
title_area.width - 1 can wrap/underflow; use saturating_sub.

🛡️ Suggested fix
-            width: title_area.width - 1,
+            width: title_area.width.saturating_sub(1),
src/screens/widgets/popup.rs-507-517 (1)

507-517: ⚠️ Potential issue | 🟡 Minor

Clamp mouse-driven synopsis scrolling.
Keyboard scrolling clamps, but mouse scroll can overshoot and show blank space.

🧭 Suggested fix
                 MouseEventKind::ScrollDown => {
-                    self.synopsis_scroll += 1;
+                    let max_scroll = self
+                        .app_info
+                        .anime_store
+                        .get(&self.anime_id)
+                        .map(|a| a.synopsis.len() as u16)
+                        .unwrap_or(self.synopsis_scroll);
+                    self.synopsis_scroll = min(self.synopsis_scroll + 1, max_scroll);
                 }
src/screens/mod.rs-86-93 (1)

86-93: ⚠️ Potential issue | 🟡 Minor

Typo: needs_accound should be needs_account.

The method and macro have a typo - "accound" should be "account". This affects the public API and should be corrected.

✏️ Proposed fix for macro and trait method
 #[macro_export]
-macro_rules! check_for_account {
+macro_rules! check_for_account {
     () => {
-        fn needs_accound(&self) -> bool {
+        fn needs_account(&self) -> bool {
             true
         }
     };
 }
     fn uses_navbar(&self) -> bool {
         true
     }
-    fn needs_accound(&self) -> bool {
+    fn needs_account(&self) -> bool {
         false
     }

And update line 330:

-            if new_screen.needs_accound() && !MalClient::user_is_logged_in() {
+            if new_screen.needs_account() && !MalClient::user_is_logged_in() {

Also applies to: 154-157

src/player/mod.rs-184-188 (1)

184-188: ⚠️ Potential issue | 🟡 Minor

Bug: Error message incorrectly says "pre-playback hook" for post-playback hook.

The error message on line 187 says "Failed to run pre-playback hook" but this is actually the post-playback hook section.

🐛 Proposed fix
         // hook
         if let Some(hook) = Config::global().player.post_playback_hook.clone()
             && let Err(e) = self.run_command(&hook, anime, episode, Some(&candidate))
         {
-            eprintln!("Failed to run pre-playback hook: {}", e);
+            eprintln!("Failed to run post-playback hook: {}", e);
         };
src/screens/overview.rs-315-315 (1)

315-315: ⚠️ Potential issue | 🟡 Minor

Replace expect() with proper error handling to prevent panics on corrupted history.

If the history file contains invalid data, expect("Failed to read history") will panic. Use ok() or proper error handling to skip malformed entries gracefully.

🛡️ Proposed fix
-                        parts[1].parse::<AnimeId>().expect("Failed to read history"),
+                        match parts[1].parse::<AnimeId>() {
+                            Ok(id) => id,
+                            Err(_) => continue, // Skip malformed entries
+                        },

Or more concisely, use ? with the surrounding logic:

                     // idk what to do with this inforamiton yet but here it is.
+                    let Ok(anime_id) = parts[1].parse::<AnimeId>() else {
+                        continue;
+                    };
                     let (
                         _timestamp,
-                        anime_id,
                         _title,
                         ...
                     ) = (
                         parts[0].to_string(),
-                        parts[1].parse::<AnimeId>().expect("Failed to read history"),
+                        anime_id,
                         parts[2].to_string(),
src/screens/list.rs-203-206 (1)

203-206: ⚠️ Potential issue | 🟡 Minor

Sorting by updated_at produces inconsistent results when the field is not populated.

The my_list_status.updated_at field defaults to "N/A" when missing. Direct string comparison via .cmp() sorts "N/A" lexicographically between other timestamps rather than grouping unpopulated entries together, breaking the intended "last updated" ordering. Consider filtering out or explicitly handling entries where updated_at == "N/A" before sorting.

src/mal/mod.rs-318-324 (1)

318-324: ⚠️ Potential issue | 🟡 Minor

Duplicate login check.

Lines 318-320 and 321-323 perform the same user_is_logged_in() check. Remove one of them.

🐛 Proposed fix
     pub fn get_anime_list(
         &self,
         status: Option<String>,
         offset: usize,
         limit: usize,
     ) -> Option<Vec<Anime>> {
         if !MalClient::user_is_logged_in() {
             return None;
         }
-        if !Self::user_is_logged_in() {
-            return None;
-        }
         self.get_anime_list_by_user("@me".to_string(), status, offset, limit)
     }
🧹 Nitpick comments (18)
src/screens/login.rs (1)

217-218: Consider handling thread join failure gracefully.

joinable.join().unwrap() will propagate a panic if the OAuth thread fails. While this runs in a background thread itself, unhandled panics can make debugging harder and leave the UI in an inconsistent state (e.g., stuck showing the URL without transitioning to success/error).

Suggested improvement
-            joinable.join().unwrap();
-            mal_client.update_user_login();
-            let new_url = "Login successful".to_string();
+            match joinable.join() {
+                Ok(_) => {
+                    mal_client.update_user_login();
+                    let new_url = "Login successful".to_string();
+                    let update = BackgroundUpdate::new(id.clone()).set("login_url", new_url);
+                    let _ = info.app_sx.send(Event::BackgroundNotice(update));
+                }
+                Err(_) => {
+                    let new_url = "Login failed".to_string();
+                    let update = BackgroundUpdate::new(id.clone()).set("login_url", new_url);
+                    let _ = info.app_sx.send(Event::BackgroundNotice(update));
+                }
+            }
+        }))
-            let update = BackgroundUpdate::new(id.clone()).set("login_url", new_url);
-            let _ = info.app_sx.send(Event::BackgroundNotice(update));
-        }))
server/Dockerfile (1)

8-10: Consider --no-install-recommends to keep the runtime image slimmer.

Small size/attack‑surface improvement, no behavior change.

♻️ Suggested change
-RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
+RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
+    && rm -rf /var/lib/apt/lists/*
server/static/index.html (1)

603-615: Handle clipboard failures gracefully.

Avoids silent failures when the Clipboard API is unavailable (e.g., non‑HTTPS contexts).

💡 Suggested change
 function copyCommand(btn) {
-    navigator.clipboard.writeText('yay -S mal').then(() => {
-        const originalText = btn.textContent;
+    const originalText = btn.textContent;
+    navigator.clipboard.writeText('yay -S mal').then(() => {
         btn.textContent = 'Copied!';
         btn.classList.add('copied');
         
         setTimeout(() => {
             btn.textContent = originalText;
             btn.classList.remove('copied');
         }, 2000);
-    });
+    }).catch(() => {
+        btn.textContent = 'Copy failed';
+        btn.classList.remove('copied');
+        setTimeout(() => {
+            btn.textContent = originalText;
+        }, 2000);
+    });
 }
server/src/main.rs (1)

271-275: Inconsistent error response format.

The root endpoint returns a 500 status with plain text on failure, while other template-loading failures (Lines 205-208, 220-223) return 400. Consider aligning error handling patterns across the codebase.

src/utils/functionStreaming.rs (1)

7-7: Minor: Typo in comment.

"changeing" should be "changing".

✏️ Fix typo
-    // for changeing
+    // for changing
deps/database/Cargo.toml (2)

7-7: Dependency name "core" shadows std library crate.

The local dependency is named core, which shadows Rust's standard library core crate. While this works due to path resolution, it may cause confusion when reading code. Consider renaming to database_core or similar.

♻️ Suggested rename
 [dependencies]
-core = { path = "./core" }
+database_core = { path = "./core", package = "core" }
 entry = { path = "./entry" }

Or rename the actual crate in ./core/Cargo.toml to database-core.


9-9: Consider pinning rusqlite to version "0.37.0" for explicit reproducibility.

Version 0.37 exists on crates.io (currently 0.37.0 is the only patch version available). While "0.37" will resolve to 0.37.0 deterministically, explicitly pinning to "0.37.0" follows reproducible builds best practices.

Cargo.toml (1)

29-30: Consider bundling SQLite for portable installs.
If you ship binaries, relying on system SQLite can cause runtime link errors on minimal distros. Enabling rusqlite’s bundled feature avoids that—please confirm it matches your distribution model.

♻️ Suggested change
-rusqlite = "0.37.0"
+rusqlite = { version = "0.37.0", features = ["bundled"] }
src/screens/list.rs (2)

600-613: Remove commented-out dead code.

This large block of commented code should be removed to improve maintainability. If this logic is needed for reference, it should be preserved in version control history instead.

🧹 Proposed fix
     fn background(&mut self) -> Option<JoinHandle<()>> {
-        // let store_animes = self.app_info.anime_store.get_list();
-        // let new_ids: Vec<AnimeId> = store_animes
-        //     .iter()
-        //     .filter(|anime| {
-        //         !self.all_animes.contains(&anime.id)
-        //             && status_is_known(anime.my_list_status.status.clone())
-        //     })
-        //     .map(|anime| anime.id)
-        //     .collect();
-        //
-        // if !new_ids.is_empty() {
-        //     self.all_animes.extend(new_ids);
-        //     self.filtered_animes = self.all_animes.clone();
-        // }
-
         if self.bg_loaded {

665-665: Minor typo: "recieved" should be "received".

✏️ Proposed fix
-                    .set("fetching", false) // updates "fetching" to false once first batch of anime is recieved
+                    .set("fetching", false) // updates "fetching" to false once first batch of anime is received
src/screens/mod.rs (1)

175-175: Consider encapsulating syncing_popup instead of making it public.

The syncing_popup field is public and accessed directly in app.rs (line 204). Consider providing methods on ScreenManager to interact with the popup state instead of exposing the field directly.

♻️ Proposed encapsulation
-    pub syncing_popup: sync::SyncPopup,
+    syncing_popup: sync::SyncPopup,

Add a method to handle sync completion:

pub fn handle_sync_status(&mut self, success: bool, anime: Anime) {
    if success {
        self.syncing_popup.finished_syncing(anime);
    } else {
        self.show_error(format!("Failed to sync {}", anime.title));
    }
}
src/app.rs (2)

231-234: Table creation on every write is inefficient.

create_table is called every time log_watched_info is invoked. Consider creating tables once during app initialization instead.

♻️ Proposed fix - move table creation to initialization

In App::new(), after creating the db_manager:

let db_manager = DatabaseManager::new(db_path).expect("Failed to initialize database manager");
db_manager.create_table::<Anime>().ok();
db_manager.create_table::<WatchHistory>().ok();

Then remove from log_watched_info:

     fn log_watched_info(&self, anime: &Anime, details: player::PlayResult) {
         let now: DateTime<Local> = Local::now();
         let history = WatchHistory {
             id: 0,
             anime_id: anime.id,
             timestamp: now.format("%Y-%m-%d %H:%M:%S").to_string(),
             episode: details.episode as i32,
             current_time: details.current_time,
             total_time: details.total_time,
             percentage: details.percentage,
             is_completed: details.is_completed,
         };
-        self.shared_info.local_db.create_table::<Anime>().ok();
-        self.shared_info.local_db.create_table::<WatchHistory>().ok();
         self.shared_info.local_db.upsert(anime.clone()).ok();
         self.shared_info.local_db.upsert(history).ok();
     }

202-210: Inconsistent brace formatting in SyncStatus handling.

The else block has inconsistent formatting compared to the rest of the codebase.

🧹 Proposed formatting fix
-                    Event::SyncStatus(success, anime ) => {
+                    Event::SyncStatus(success, anime) => {
                         if success {
                             self.screen_manager.syncing_popup.finished_syncing(*anime);
-                        }
-                        else {
+                        } else {
                             self.screen_manager.show_error(format!("Failed to sync {}", anime.title));
                         }
-
                     }
src/screens/launch.rs (1)

64-64: Remove commented debug code.

🧹 Proposed fix
             2 => Some(Action::Quit),
-            // 2 => Some(Action::SwitchScreen(LAUNCH)),
             _ => None,
src/mal/network.rs (1)

342-369: Consider returning a more descriptive error from update_local.

The error message "local db error" on line 364 loses the original error context. Consider preserving the original error for better debugging.

Proposed fix
     fn update_local(
         mut self,
         database: &DatabaseManager,
     ) -> Result<(usize, Self::Response), Box<dyn std::error::Error>>
     {
         self.pre_update();
         let updated = match database.upsert(self) {
             Ok(u) => u,
             Err(e) => {
                 send_error!("Failed to update local database: {}", e);
-                return Err("local db error".into());
+                return Err(format!("Failed to update local database: {}", e).into());
             }
         };
         let response = updated.to_offline_response();
         Ok((updated.get_id(), response))
     }
src/screens/widgets/sync.rs (2)

83-95: Typo in method name: change_button_lables should be change_button_labels.

Proposed fix
-    fn change_button_lables(&mut self) {
+    fn change_button_labels(&mut self) {

Also update the call sites at lines 231 and 310.


716-720: Consider using more descriptive button labels.

The labels "plural" and "single" don't clearly communicate the toggle state to users. Consider more descriptive text like "Apply to All" / "One at a Time" or using icons/symbols.

src/mal/mod.rs (1)

193-209: Consider adding error logging when client ID fetch fails.

The get_client_id() method silently returns None on failure (line 205 uses .ok()?). Adding a log/error message would aid debugging when the auth server is unreachable.

Proposed improvement
         let client_id = send_request_expect_text(
             "GET",
             format!("{}/id", Config::global().network.auth_server),
             vec![],
             vec![],
             None,
         )
-        .ok()?;
+        .map_err(|e| {
+            send_error!("Failed to fetch client ID: {}", e);
+            e
+        })
+        .ok()?;

Comment thread deps/database/core/Cargo.toml
Comment thread deps/database/core/src/lib.rs
Comment thread deps/database/core/src/lib.rs
Comment thread deps/database/core/src/lib.rs
Comment on lines +207 to +221
quote! {
self.#field_name.as_ref()
.map(|v| serde_json::to_string(v).unwrap())
.map(|s| rusqlite::types::Value::from(s))
.unwrap_or(rusqlite::types::Value::Null)
}
}
} else {
quote! { rusqlite::types::Value::Null }
}
} else {
quote! {
rusqlite::types::Value::from(serde_json::to_string(&self.#field_name).unwrap())
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Generated code uses unwrap() on JSON serialization - runtime panic risk.

The generated bind_values code uses .unwrap() on serde_json::to_string() calls (Lines 209, 219). If serialization fails, this will panic at runtime.

🛠️ Consider returning an error or using expect with context

For complex types, consider:

-            rusqlite::types::Value::from(serde_json::to_string(&self.#field_name).unwrap())
+            rusqlite::types::Value::from(
+                serde_json::to_string(&self.#field_name)
+                    .expect(concat!("Failed to serialize field: ", stringify!(`#field_name`)))
+            )

Or better yet, consider if bind_values should return Result<Vec<...>, Error>.

📝 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
quote! {
self.#field_name.as_ref()
.map(|v| serde_json::to_string(v).unwrap())
.map(|s| rusqlite::types::Value::from(s))
.unwrap_or(rusqlite::types::Value::Null)
}
}
} else {
quote! { rusqlite::types::Value::Null }
}
} else {
quote! {
rusqlite::types::Value::from(serde_json::to_string(&self.#field_name).unwrap())
}
}
quote! {
self.#field_name.as_ref()
.map(|v| serde_json::to_string(v).unwrap())
.map(|s| rusqlite::types::Value::from(s))
.unwrap_or(rusqlite::types::Value::Null)
}
}
} else {
quote! { rusqlite::types::Value::Null }
}
} else {
quote! {
rusqlite::types::Value::from(
serde_json::to_string(&self.#field_name)
.expect(concat!("Failed to serialize field: ", stringify!(`#field_name`)))
)
}
}
🤖 Prompt for AI Agents
In `@deps/database/entry/src/lib.rs` around lines 207 - 221, The generated code in
bind_values uses serde_json::to_string(...).unwrap() for complex fields
(occurrences of serde_json::to_string in the quote! blocks), which can panic;
change the generator so bind_values propagates serialization errors instead of
unwrapping: update the bind_values signature to return Result<..., Error> (or
Result<Vec<rusqlite::types::Value>, rusqlite::Error/any appropriate error type)
and replace each .unwrap() with ? or map_err(...) to convert the
serde_json::Error into the chosen error type; ensure both branches that
currently produce
rusqlite::types::Value::from(serde_json::to_string(...).unwrap()) and the
.map(|v| serde_json::to_string(v).unwrap()) form use fallible serialization that
returns Err on failure so the caller can handle the error.

Comment thread server/src/main.rs
Comment thread src/mal/models/mod.rs
Comment thread src/screens/search.rs
Comment thread src/screens/widgets/popup.rs
Comment thread src/screens/widgets/sync.rs
Copy link
Copy Markdown

@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: 6

🤖 Fix all issues with AI agents
In `@deps/database/core/src/lib.rs`:
- Around line 144-153: The clear_force function toggles PRAGMA foreign_keys
around a DELETE but is not atomic: if the DELETE fails and re-enabling FKs also
fails you can lose the original error or leave DB in inconsistent state. Modify
clear_force (and reference T::table_name and Entryable) to run the FK toggle and
DELETE inside a robust scope that always restores the original FK state (use a
transaction or a guard pattern) so that foreign_keys is re-enabled even on
panic/early return; capture and propagate both errors correctly (prefer
returning the primary error and chaining the FK-restoration error) to ensure the
DB FK state is always restored.
- Around line 122-134: The delete method currently interpolates the primary key
value directly into the SQL string (in pub fn delete<T: Entryable>, using
T::p_key_column() and obj.p_key()); change it to use a parameterized query
instead: build the query with a placeholder for the p_key (e.g. "DELETE FROM
{table} WHERE {p_key_column} = ?") and pass obj.p_key() as the parameter to
connection.execute rather than embedding it into the SQL string so the value is
safely bound at execution time.

In `@src/app.rs`:
- Around line 224-240: In log_watched_info, don't silently ignore database
errors by calling .ok() on the Results returned from create_table::<Anime>(),
create_table::<WatchHistory>(), upsert(anime.clone()), and upsert(history);
instead handle each Result explicitly: match or use .map_err/.inspect_err to log
failures (e.g., via an existing logger in self.shared_info or tracing::error)
and surface or return errors if appropriate so callers can know when table
creation or upsert failed; update log_watched_info to propagate errors (change
signature to return Result<_, DbError> if necessary) or at minimum record
descriptive error messages including the operation and error value for
create_table::<Anime>(), create_table::<WatchHistory>(), upsert(anime.clone()),
and upsert(history).

In `@src/screens/widgets/popup.rs`:
- Around line 669-674: The info_area Rect sets width using title_area.width - 1
which can underflow for u16 in tiny terminals; update the calculation in the
info_area construction to use title_area.width.saturating_sub(1) (i.e., replace
the subtraction with saturating_sub on title_area.width) so the width never
wraps below zero while keeping the field type consistent.

In `@src/screens/widgets/sync.rs`:
- Around line 47-74: The comment for Navigatable dimensions is misleading: in
SyncPopup::new the call Navigatable::new((1, buttons.len() as u16)) creates 1
column and 2 rows, but the comment says "2 columns, 1 row"; fix this by swapping
the tuple to Navigatable::new((buttons.len() as u16, 1)) so it creates 2 columns
and 1 row (or alternatively update the comment to accurately describe the
current tuple); update the nav initialization in SyncPopup::new accordingly.
- Around line 83-95: Rename the method change_button_lables to
change_button_labels in the impl for the sync widget and update every call site
to use the new name; specifically change the method signature and body name in
src/screens/widgets/sync.rs (the impl that sets self.buttons based on
self.all_selected) and replace any references to change_button_lables with
change_button_labels where the method is invoked so the compiler and callers
remain consistent.
🧹 Nitpick comments (3)
src/app.rs (1)

202-215: Inconsistent brace formatting.

The else blocks use different formatting styles. Consider consistent formatting for readability.

♻️ Suggested formatting
                     Event::SyncStatus(success, sync, anime ) => {
                         if success {
                             if sync {
                                 self.screen_manager.syncing_popup.finished_syncing(*anime);
-                            }
-                            else{
+                            } else {
                                 self.screen_manager.syncing_popup.finished_deleting(*anime);
                             }
-                        }
-                        else {
+                        } else {
                             self.screen_manager.show_error(format!("Failed to sync {}", anime.title));
                         }
-
                     }
src/screens/widgets/sync.rs (2)

640-690: Redundant None handling in remote info rendering.

The code checks mal_anime.is_none() at Line 640 and renders "Not added to list." However, the else branch (Line 651) immediately matches mal_anime.as_ref() and handles the None case again (Lines 653-656), which is unreachable.

♻️ Simplify by removing redundant branch
-        if mal_anime.is_none() {
-            // No remote info available
-            let no_remote_info = vec![Line::from(vec![Span::styled(
-                "Not added to list.",
-                Style::default().fg(Config::global().theme.error),
-            )])];
-
-            let remote_paragraph = Paragraph::new(no_remote_info)
-                .wrap(Wrap { trim: true })
-                .block(Block::default().padding(Padding::new(1, 1, 1, 1)));
-            frame.render_widget(remote_paragraph, remote_text_area);
-        } else {
-            let remote_info_text = match mal_anime.as_ref() {
-                None => vec![Line::from(vec![Span::styled(
-                    "Not added to list.",
-                    Style::default().fg(Config::global().theme.error),
-                )])],
-                Some(remote) => vec![
+        let remote_info_text = match mal_anime.as_ref() {
+            None => vec![Line::from(vec![Span::styled(
+                "Not added to list.",
+                Style::default().fg(Config::global().theme.error),
+            )])],
+            Some(remote) => vec![
                     Line::from(vec![
                         Span::raw("Status: "),
                         // ... rest of remote info
                     ]),
                     // ...
-                ],
-            };
-
-            let remote_paragraph = Paragraph::new(remote_info_text)
-                .wrap(Wrap { trim: true })
-                .block(Block::default().padding(Padding::new(1, 1, 1, 1)));
-
-            frame.render_widget(remote_paragraph, remote_text_area);
-        }
+            ],
+        };
+
+        let remote_paragraph = Paragraph::new(remote_info_text)
+            .wrap(Wrap { trim: true })
+            .block(Block::default().padding(Padding::new(1, 1, 1, 1)));
+
+        frame.render_widget(remote_paragraph, remote_text_area);

716-732: Non-descriptive button labels "plural" and "single".

The batch button displays "plural" or "single" which may be confusing to users. Consider more descriptive labels like "All" / "One" or using icons/symbols.

💡 Suggested improvement
         let all_btn = if self.all_selected {
-            "plural"
+            "[All]"
         } else {
-            "single"
+            "[One]"
         };

Or consider using a checkbox-style indicator like "☑ All" / "☐ All".

Comment on lines +122 to +134
// delete
pub fn delete<T: Entryable>(&self, obj: &T) -> Result<(), Error> {
let connection = self.connection.lock().unwrap();
let table_name = T::table_name();
let condition = format!("{} = {}", T::p_key_column(), obj.p_key());
let query = format!(
"DELETE FROM {} WHERE {}",
table_name,
condition
);
connection.execute(&query, [])?;
Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Primary key value should be parameterized.

While p_key_column() addresses the column name concern, the primary key value obj.p_key() is still interpolated directly into SQL (Line 126). This should use a parameterized query for safety and consistency with other methods.

🔒 Proposed fix to parameterize the primary key value
     pub fn delete<T: Entryable>(&self, obj: &T) -> Result<(), Error> {
         let connection = self.connection.lock().unwrap();
         let table_name = T::table_name();
-        let condition = format!("{} = {}", T::p_key_column(), obj.p_key());
-        let query = format!(
-            "DELETE FROM {} WHERE {}",
-            table_name,
-            condition
-        );
-        connection.execute(&query, [])?;
+        let query = format!(
+            "DELETE FROM {} WHERE {} = ?1",
+            table_name,
+            T::p_key_column()
+        );
+        connection.execute(&query, [obj.p_key()])?;
         Ok(())
     }
🤖 Prompt for AI Agents
In `@deps/database/core/src/lib.rs` around lines 122 - 134, The delete method
currently interpolates the primary key value directly into the SQL string (in
pub fn delete<T: Entryable>, using T::p_key_column() and obj.p_key()); change it
to use a parameterized query instead: build the query with a placeholder for the
p_key (e.g. "DELETE FROM {table} WHERE {p_key_column} = ?") and pass obj.p_key()
as the parameter to connection.execute rather than embedding it into the SQL
string so the value is safely bound at execution time.

Comment on lines +144 to +153
// clear all rows, ignoring foreign key constraints
pub fn clear_force<T: Entryable>(&self) -> Result<(), Error> {
let connection = self.connection.lock().unwrap();
let table_name = T::table_name();
connection.execute("PRAGMA foreign_keys = OFF", [])?;
let result = connection.execute(&format!("DELETE FROM {}", table_name), []);
connection.execute("PRAGMA foreign_keys = ON", [])?;
result?;
Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Foreign key toggle is not atomic with the delete operation.

If the delete fails (Line 149), foreign keys are re-enabled (Line 150), but if re-enabling also fails, the error from the delete is returned while the FK state might be inconsistent. Consider using a transaction or ensuring the FK state is always restored.

🛡️ Proposed fix using a guard pattern
     pub fn clear_force<T: Entryable>(&self) -> Result<(), Error> {
         let connection = self.connection.lock().unwrap();
         let table_name = T::table_name();
         connection.execute("PRAGMA foreign_keys = OFF", [])?;
-        let result = connection.execute(&format!("DELETE FROM {}", table_name), []);
-        connection.execute("PRAGMA foreign_keys = ON", [])?;
-        result?;
+        let result = connection.execute(&format!("DELETE FROM {}", table_name), []);
+        // Always re-enable foreign keys, even if delete failed
+        let _ = connection.execute("PRAGMA foreign_keys = ON", []);
+        result?;
         Ok(())
     }
📝 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
// clear all rows, ignoring foreign key constraints
pub fn clear_force<T: Entryable>(&self) -> Result<(), Error> {
let connection = self.connection.lock().unwrap();
let table_name = T::table_name();
connection.execute("PRAGMA foreign_keys = OFF", [])?;
let result = connection.execute(&format!("DELETE FROM {}", table_name), []);
connection.execute("PRAGMA foreign_keys = ON", [])?;
result?;
Ok(())
}
// clear all rows, ignoring foreign key constraints
pub fn clear_force<T: Entryable>(&self) -> Result<(), Error> {
let connection = self.connection.lock().unwrap();
let table_name = T::table_name();
connection.execute("PRAGMA foreign_keys = OFF", [])?;
let result = connection.execute(&format!("DELETE FROM {}", table_name), []);
// Always re-enable foreign keys, even if delete failed
let _ = connection.execute("PRAGMA foreign_keys = ON", []);
result?;
Ok(())
}
🤖 Prompt for AI Agents
In `@deps/database/core/src/lib.rs` around lines 144 - 153, The clear_force
function toggles PRAGMA foreign_keys around a DELETE but is not atomic: if the
DELETE fails and re-enabling FKs also fails you can lose the original error or
leave DB in inconsistent state. Modify clear_force (and reference T::table_name
and Entryable) to run the FK toggle and DELETE inside a robust scope that always
restores the original FK state (use a transaction or a guard pattern) so that
foreign_keys is re-enabled even on panic/early return; capture and propagate
both errors correctly (prefer returning the primary error and chaining the
FK-restoration error) to ensure the DB FK state is always restored.

Comment thread src/app.rs
Comment on lines +224 to 240
fn log_watched_info(&self, anime: &Anime, details: player::PlayResult) {
let now: DateTime<Local> = Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S");
let log_file = app_dir.join("watch_history");
let log_entry = format!(
"{} -> {} -> \"{}\" -> {} -> {}/{} -> {} -> {}\n",
timestamp,
anime.id,
anime.title,
details.episode,
details.current_time,
details.total_time,
details.percentage,
details.completed
);

let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(log_file)
.expect("Failed to open log file");

file.write_all(log_entry.as_bytes()).ok();
let history = WatchHistory {
id: 0,
anime_id: anime.id,
timestamp: now.format("%Y-%m-%d %H:%M:%S").to_string(),
episode: details.episode as i32,
current_time: details.current_time,
total_time: details.total_time,
percentage: details.percentage,
is_completed: details.is_completed,
};
self.shared_info.local_db.create_table::<Anime>().ok();
self.shared_info.local_db.create_table::<WatchHistory>().ok();
self.shared_info.local_db.upsert(anime.clone()).ok();
self.shared_info.local_db.upsert(history).ok();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent error handling may hide database issues.

Using .ok() on Lines 236-239 silently discards any database errors. If table creation or upsert fails, the user won't know their watch history wasn't saved.

🛠️ Suggested improvement to log errors
-        self.shared_info.local_db.create_table::<Anime>().ok();
-        self.shared_info.local_db.create_table::<WatchHistory>().ok();
-        self.shared_info.local_db.upsert(anime.clone()).ok();
-        self.shared_info.local_db.upsert(history).ok();
+        if let Err(e) = self.shared_info.local_db.create_table::<Anime>() {
+            errorBus::push(format!("Failed to create Anime table: {}", e));
+        }
+        if let Err(e) = self.shared_info.local_db.create_table::<WatchHistory>() {
+            errorBus::push(format!("Failed to create WatchHistory table: {}", e));
+        }
+        if let Err(e) = self.shared_info.local_db.upsert(anime.clone()) {
+            errorBus::push(format!("Failed to save anime: {}", e));
+        }
+        if let Err(e) = self.shared_info.local_db.upsert(history) {
+            errorBus::push(format!("Failed to save watch history: {}", e));
+        }
🤖 Prompt for AI Agents
In `@src/app.rs` around lines 224 - 240, In log_watched_info, don't silently
ignore database errors by calling .ok() on the Results returned from
create_table::<Anime>(), create_table::<WatchHistory>(), upsert(anime.clone()),
and upsert(history); instead handle each Result explicitly: match or use
.map_err/.inspect_err to log failures (e.g., via an existing logger in
self.shared_info or tracing::error) and surface or return errors if appropriate
so callers can know when table creation or upsert failed; update
log_watched_info to propagate errors (change signature to return Result<_,
DbError> if necessary) or at minimum record descriptive error messages including
the operation and error value for create_table::<Anime>(),
create_table::<WatchHistory>(), upsert(anime.clone()), and upsert(history).

Comment on lines 669 to 674
let info_area = Rect {
x: title_area.x,
y: info_area.y,
width: title_area.width-1,
width: title_area.width - 1,
height: info_area.height.saturating_sub(buttons_area.height),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid potential u16 underflow in info area width.
Line 672 subtracts 1 from a u16. If the width ever reaches 0 in tiny terminals, this will wrap. Prefer saturating_sub(1).

🐛 Safer width calculation
-            width: title_area.width - 1,
+            width: title_area.width.saturating_sub(1),
📝 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
let info_area = Rect {
x: title_area.x,
y: info_area.y,
width: title_area.width-1,
width: title_area.width - 1,
height: info_area.height.saturating_sub(buttons_area.height),
};
let info_area = Rect {
x: title_area.x,
y: info_area.y,
width: title_area.width.saturating_sub(1),
height: info_area.height.saturating_sub(buttons_area.height),
};
🤖 Prompt for AI Agents
In `@src/screens/widgets/popup.rs` around lines 669 - 674, The info_area Rect sets
width using title_area.width - 1 which can underflow for u16 in tiny terminals;
update the calculation in the info_area construction to use
title_area.width.saturating_sub(1) (i.e., replace the subtraction with
saturating_sub on title_area.width) so the width never wraps below zero while
keeping the field type consistent.

Comment on lines +47 to +74
impl SyncPopup {
pub fn new(info: ExtraInfo) -> Self {
let buttons = vec![
"Sync to MAL".to_string(),
"Remove local changes".to_string(),
];

let image_manager = Arc::new(Mutex::new(ImageManager::new()));

ImageManager::init_with_threads(&image_manager, info.app_sx.clone());

Self {
toggled: false,
syncing: false,
current_anime: 0,
animes_to_sync: Vec::new(),
to_be_processed: Vec::new(),
finished_processing: Vec::new(),
nav: Navigatable::new((1, buttons.len() as u16)), // 2 columns, 1 row
buttons,
image_manager,
info,
all_selected: false,
focus: FocusedElement::SyncButtons,
back_btn_area: None,
batch_btn_area: None,
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment doesn't match Navigatable dimensions.

Line 65 comment says "2 columns, 1 row" but the constructor Navigatable::new((1, buttons.len() as u16)) appears to create 1 column with 2 rows based on the tuple order.

📝 Fix the misleading comment
-            nav: Navigatable::new((1, buttons.len() as u16)), // 2 columns, 1 row
+            nav: Navigatable::new((1, buttons.len() as u16)), // 1 column, 2 rows (or verify tuple order)
📝 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
impl SyncPopup {
pub fn new(info: ExtraInfo) -> Self {
let buttons = vec![
"Sync to MAL".to_string(),
"Remove local changes".to_string(),
];
let image_manager = Arc::new(Mutex::new(ImageManager::new()));
ImageManager::init_with_threads(&image_manager, info.app_sx.clone());
Self {
toggled: false,
syncing: false,
current_anime: 0,
animes_to_sync: Vec::new(),
to_be_processed: Vec::new(),
finished_processing: Vec::new(),
nav: Navigatable::new((1, buttons.len() as u16)), // 2 columns, 1 row
buttons,
image_manager,
info,
all_selected: false,
focus: FocusedElement::SyncButtons,
back_btn_area: None,
batch_btn_area: None,
}
}
impl SyncPopup {
pub fn new(info: ExtraInfo) -> Self {
let buttons = vec![
"Sync to MAL".to_string(),
"Remove local changes".to_string(),
];
let image_manager = Arc::new(Mutex::new(ImageManager::new()));
ImageManager::init_with_threads(&image_manager, info.app_sx.clone());
Self {
toggled: false,
syncing: false,
current_anime: 0,
animes_to_sync: Vec::new(),
to_be_processed: Vec::new(),
finished_processing: Vec::new(),
nav: Navigatable::new((1, buttons.len() as u16)), // 1 column, 2 rows (or verify tuple order)
buttons,
image_manager,
info,
all_selected: false,
focus: FocusedElement::SyncButtons,
back_btn_area: None,
batch_btn_area: None,
}
}
🤖 Prompt for AI Agents
In `@src/screens/widgets/sync.rs` around lines 47 - 74, The comment for
Navigatable dimensions is misleading: in SyncPopup::new the call
Navigatable::new((1, buttons.len() as u16)) creates 1 column and 2 rows, but the
comment says "2 columns, 1 row"; fix this by swapping the tuple to
Navigatable::new((buttons.len() as u16, 1)) so it creates 2 columns and 1 row
(or alternatively update the comment to accurately describe the current tuple);
update the nav initialization in SyncPopup::new accordingly.

Comment on lines +83 to +95
fn change_button_lables(&mut self) {
if self.all_selected {
self.buttons = vec![
"Sync to MAL (all)".to_string(),
"Remove local changes (all)".to_string(),
];
} else {
self.buttons = vec![
"Sync to MAL".to_string(),
"Remove local changes".to_string(),
];
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in method name: "lables" should be "labels".

📝 Fix typo
-    fn change_button_lables(&mut self) {
+    fn change_button_labels(&mut self) {

Also update the call sites at lines 231 and 310.

🤖 Prompt for AI Agents
In `@src/screens/widgets/sync.rs` around lines 83 - 95, Rename the method
change_button_lables to change_button_labels in the impl for the sync widget and
update every call site to use the new name; specifically change the method
signature and body name in src/screens/widgets/sync.rs (the impl that sets
self.buttons based on self.all_selected) and replace any references to
change_button_lables with change_button_labels where the method is invoked so
the compiler and callers remain consistent.

@swstl swstl merged commit 4a51118 into main Feb 3, 2026
@swstl swstl deleted the no-account branch February 3, 2026 22:21
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.

1 participant