Skip to content

Add Dracula app theme#38

Merged
hawkff merged 4 commits into
mainfrom
feature/dracula-theme
Jun 20, 2026
Merged

Add Dracula app theme#38
hawkff merged 4 commits into
mainfrom
feature/dracula-theme

Conversation

@hawkff

@hawkff hawkff commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Dracula as a new app theme (theme option 23), using the official Dracula palette.

  • Purple primary (#bd93f9), pink accent (#ff79c6), with matching dark/100/300 tints.
  • Full dark canvas — #282a36 background, #343746 surface, #f8f8f2 foreground — applied via values-night/themes.xml overrides scoped to the Dracula style only. Other themes keep the Material DayNight defaults untouched.
  • Because Dracula is dark-only, selecting it forces night mode on so the dark canvas takes effect. The user's previous night-mode setting is remembered and restored when switching to another theme. A manual night-mode change clears the pending restore so the user's choice always wins.

Implementation

Wired into the existing index-driven theme system (same pattern as VerdantMint = 22):

  • Theme.ktDRACULA = 23 + branches in getTheme / getDialogTheme.
  • values/colors.xml — Dracula colors + swatch entry (array index 23 == theme ID).
  • values/themes.xmlTheme.SagerNet.Dracula and Theme.SagerNet.Dialog.Dracula (accent styles, any mode).
  • values-night/themes.xml (new) — true dark surfaces for Dracula in night mode.
  • values-night/colors.xml — dark surface colors + elevated card.
  • Constants.kt / DataStore.ktnightThemeBeforeDracula field for save/restore.
  • SettingsPreferenceFragment.kt — force/restore night-mode logic.

Testing

  • assembleOssDebug builds clean; verified on Android (arm64-v8a debug, installed on device).
  • CodeRabbit CLI review: 0 findings.

Greptile Summary

This PR adds Dracula (theme ID 23) as a new app theme using the official Dracula palette — purple primary (#bd93f9), pink accent (#ff79c6), and a full dark canvas (#282a36/#343746/#f8f8f2) applied via values-night overrides. Because Dracula is dark-only, selecting it forces night mode on, saving and restoring the previous night-mode setting when switching away.

  • Theme wiring follows the existing index-driven pattern (same as VerdantMint = 22): constant in Theme.kt, when branches in getTheme/getDialogTheme, swatch appended to material_colors at the correct index (22 → theme ID 23), and accent + night-override styles in all relevant values[-night][-v26]/themes.xml files.
  • Night-mode save/restore is implemented via a new nightThemeBeforeDracula persistent field (default -1 sentinel); entering Dracula saves and forces night mode on, leaving restores, and a manual night-mode change clears the saved value so the user's choice always wins.
  • Base-theme fix: itemShapeFillColor = ?colorPrimary is added to Theme.SagerNet in both values/themes.xml and values-v26/themes.xml to resolve a ?itemShapeFillColor self-reference in the nav-drawer ColorStateList that would affect any theme without an explicit override.

Confidence Score: 5/5

Safe to merge; the theme wiring, color definitions, and night-mode save/restore logic are all correct and self-contained.

The change is additive — a new theme constant, new resource files, and a save/restore mechanism for night mode. The array index alignment is correct, the sentinel value logic is sound, and no existing theme behaviour is modified. The only observed issue is a double activity recreation (one from applyNightTheme and one from the explicit recreate call) when switching night mode alongside the theme, which produces a visible extra blink but no data loss or crash.

SettingsPreferenceFragment.kt — the Dracula enter/exit branches call both applyNightTheme() and ActivityCompat.recreate(), which can trigger two back-to-back recreations.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt Adds Dracula night-mode force/restore logic to the theme change listener; double-recreation possible when night mode is also changed
app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt Adds DRACULA = 23 constant and corresponding branches in getTheme/getDialogTheme; follows the identical pattern as VerdantMint
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt Adds nightThemeBeforeDracula delegated property with default -1 sentinel; uses configurationStore.int correctly, separate from the stringToInt nightTheme field
app/src/main/res/values-night/themes.xml New file; declares Dracula and Dracula Dialog night-only surface overrides; correctly scoped to Dracula styles only, leaving other themes untouched
app/src/main/res/values/themes.xml Adds Theme.SagerNet.Dracula and Theme.SagerNet.Dialog.Dracula accent styles; also adds itemShapeFillColor to base theme and dialog base to fix nav-drawer ColorStateList self-reference
app/src/main/res/values/colors.xml Adds Dracula color palette and appends color_dracula to material_colors array at index 22 — correctly aligns with DRACULA = 23
app/src/main/res/values-night/colors.xml Adds dark-surface Dracula colors (background #282A36, surface #343746, on-surface #F8F8F2) for night-mode overrides
app/src/main/res/values-v26/themes.xml Adds itemShapeFillColor = ?colorPrimary to the API 26+ base theme to satisfy the nav-drawer ColorStateList self-reference

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([User selects theme]) --> B{New theme\n== Dracula?}
    B -- Yes --> C{Previous theme\nalready Dracula?}
    C -- Yes --> G
    C -- No --> D{nightTheme\nalready == 1?}
    D -- Yes --> G[Apply new theme\nrecreate activity]
    D -- No --> E[Save nightTheme\nto nightThemeBeforeDracula]
    E --> F[Force night mode on\nnightTheme.value = 1\napplyNightTheme]
    F --> G
    B -- No --> H{Previous theme\n== Dracula?}
    H -- No --> G
    H -- Yes --> I{nightThemeBeforeDracula\n!= -1?}
    I -- No --> G
    I -- Yes --> J[Reset nightThemeBeforeDracula = -1\nRestore saved night mode\napplyNightTheme]
    J --> G
    K([User changes night mode manually]) --> L[Clear nightThemeBeforeDracula = -1\napplyNightTheme]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A([User selects theme]) --> B{New theme\n== Dracula?}
    B -- Yes --> C{Previous theme\nalready Dracula?}
    C -- Yes --> G
    C -- No --> D{nightTheme\nalready == 1?}
    D -- Yes --> G[Apply new theme\nrecreate activity]
    D -- No --> E[Save nightTheme\nto nightThemeBeforeDracula]
    E --> F[Force night mode on\nnightTheme.value = 1\napplyNightTheme]
    F --> G
    B -- No --> H{Previous theme\n== Dracula?}
    H -- No --> G
    H -- Yes --> I{nightThemeBeforeDracula\n!= -1?}
    I -- No --> G
    I -- Yes --> J[Reset nightThemeBeforeDracula = -1\nRestore saved night mode\napplyNightTheme]
    J --> G
    K([User changes night mode manually]) --> L[Clear nightThemeBeforeDracula = -1\napplyNightTheme]
Loading

Reviews (3): Last reviewed commit: "Resolve nav-drawer itemShapeFillColor wa..." | Re-trigger Greptile

Add Dracula as theme option 23 with the official palette (purple primary,
pink accent). The theme paints a full dark canvas (#282a36 background,
#343746 surface, #f8f8f2 foreground) via values-night overrides scoped to
the Dracula style only; other themes keep the Material DayNight defaults.

Selecting Dracula forces night mode on so its dark canvas applies, and the
user's prior night-mode setting is remembered and restored when switching
to another theme. A manual night-mode change clears the pending restore.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

hawkff has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Dracula theme (constant id=23) with color palette resources and day/night XML styles, registers it in Theme.kt's dispatch functions, introduces a nightThemeBeforeDracula DataStore property to persist prior night-mode state, and updates the settings preference listeners to force night mode on when entering Dracula and restore the previous value on exit.

Changes

Dracula Theme

Layer / File(s) Summary
Dracula color palette and XML theme styles
app/src/main/res/values/colors.xml, app/src/main/res/values-night/colors.xml, app/src/main/res/values/themes.xml, app/src/main/res/values-night/themes.xml, app/src/main/res/values-v26/themes.xml
Adds Dracula palette entries (color_dracula, color_dracula_dark, color_dracula_accent, color_dracula_100, color_dracula_300, card_elevated_dracula) and night-mode dark surface colors, registers @color/color_dracula in the material_colors array, and declares Theme.SagerNet.Dracula and Theme.SagerNet.Dialog.Dracula styles for both day and night resource qualifiers. Adds itemShapeFillColor defaults in values and v26 theme files.
Theme constant and style dispatch
app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt
Adds DRACULA = 23 to the Theme object and extends getTheme() and getDialogTheme() to return the new Dracula styles for that value.
Night-mode save/restore state
app/src/main/java/io/nekohasekai/sagernet/Constants.kt, app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
Adds the NIGHT_THEME_BEFORE_DRACULA key constant and the nightThemeBeforeDracula integer property (default -1) on DataStore to track the user's night-mode setting before Dracula forces it on.
Settings preference Dracula enter/leave logic
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
Expands the appTheme listener to save DataStore.nightTheme and force night mode when entering Dracula, and to restore the saved value when leaving Dracula. Updates the nightTheme listener to clear nightThemeBeforeDracula on any manual night-mode change.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant appThemeListener
  participant DataStore
  participant nightThemeListener

  rect rgba(138, 43, 226, 0.5)
    Note over User,DataStore: Entering Dracula
    User->>appThemeListener: select Dracula theme
    appThemeListener->>DataStore: read nightTheme → save to nightThemeBeforeDracula
    appThemeListener->>DataStore: write nightTheme = force_on
    appThemeListener->>appThemeListener: applyNightTheme + recreate activity
  end

  rect rgba(75, 0, 130, 0.5)
    Note over User,DataStore: Leaving Dracula
    User->>appThemeListener: select non-Dracula theme
    appThemeListener->>DataStore: read nightThemeBeforeDracula
    appThemeListener->>DataStore: write nightTheme = restored value
    appThemeListener->>DataStore: write nightThemeBeforeDracula = -1
    appThemeListener->>appThemeListener: applyNightTheme + recreate activity
  end

  rect rgba(64, 64, 64, 0.5)
    Note over User,nightThemeListener: Manual override
    User->>nightThemeListener: change night mode manually
    nightThemeListener->>DataStore: write nightThemeBeforeDracula = -1
    nightThemeListener->>nightThemeListener: applyNightTheme
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • hawkff/NekoBoxForAndroid#19: The main PR's Dracula theme styles explicitly override cardElevatedSurfaceColor, tying directly into the elevated-card theming/attribute introduced in that PR.

Poem

🐇 A cape of purple in the night,
Colors dark as Dracula's bite.
Night mode forced with fangs of code,
Restored when leaving shadow's abode.
The rabbit hops through themes with glee—
Dark palette shipped, now let it be! 🦇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Dracula app theme' directly and clearly describes the main objective of the changeset: adding a new Dracula theme to the application.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description comprehensively details the addition of the Dracula theme, explaining its design, implementation approach, color palette, night-mode logic, and verification steps—directly matching the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

On-device logcat showed a recoverable ResourcesCompat warning: the
NavigationView's @color/navigation_item ColorStateList self-references
?itemShapeFillColor, which the base theme never sets (only the Black theme
does). Set it on the Dracula day and night styles so the selected nav-drawer
item gets a proper Dracula-purple fill and the warning no longer fires.

Pre-existing on all non-Black themes; fixed here for the new Dracula theme.
Verified on Android: warning count 0, no crash.
hawkff added 2 commits June 19, 2026 22:48
nightTheme.value (ListPreference.setValue -> persistString) already writes
the same configurationStore key that DataStore.nightTheme (stringToInt ->
putString) uses, and refreshes the picker UI. Remove the duplicate
DataStore.nightTheme assignments in both the force-on and restore branches.

Verified on-device: appTheme persists as 23 (Dracula), nightTheme persists
as a string in the same format; no crash, no resource warnings.
Addresses Greptile P2.
@color/navigation_item self-references ?itemShapeFillColor, which the base
Theme.SagerNet never set, so the ColorStateList failed to resolve (a
recoverable ResourcesCompat warning) on every theme except Black. On-device
theme-switching confirmed it firing on Brown/Grey/Red/VerdantMint.

Set itemShapeFillColor=?colorPrimary on the base Theme.SagerNet, its Dialog
variant, and the v26 base, fixing it repo-wide. Drop the now-redundant
per-theme overrides added to Dracula (day + night); Black keeps its explicit
override. Verified on device (API 33): Dracula, Grey, and Black all start
with 0 warnings and no crash.
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