Skip to content

Commit d6e2939

Browse files
authored
fix: enhance dark mode support in theme handling (#1808)
- Added PHP logic to determine if the current theme is dark and set a CSS variable accordingly. - Introduced a new function to retrieve the dark mode state from the CSS variable in JavaScript. - Updated the theme store to initialize dark mode based on the CSS variable, ensuring consistent theme application across the application. This improves user experience by ensuring the correct theme is applied based on user preferences. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Server-persisted theme mutation and client action to fetch/apply themes * **Improvements** * Safer theme parsing and multi-source initialization (CSS var, storage, cookie, server) * Robust dark-mode detection and propagation across document, modals and teleport containers * Responsive banner/header gradient handling with tunable CSS variables and fallbacks * **Tests** * Expanded tests for theme flows, dark-mode detection, banner gradients and manifest robustness <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 317e0fa commit d6e2939

31 files changed

Lines changed: 1457 additions & 249 deletions

‎@tailwind-shared/base-utilities.css‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,6 @@
6363
*/
6464

6565
.unapi {
66-
--color-alpha: #1c1b1b;
67-
--color-beta: #f2f2f2;
68-
--color-gamma: #999999;
69-
--color-gamma-opaque: rgba(153, 153, 153, 0.5);
70-
--color-customgradient-start: rgba(242, 242, 242, 0);
71-
--color-customgradient-end: rgba(242, 242, 242, 0.85);
72-
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, 0.15);
73-
--ring-offset-shadow: 0 0 var(--color-beta);
74-
--ring-shadow: 0 0 var(--color-beta);
7566
}
7667

7768
.unapi button:not(:disabled),

‎@tailwind-shared/theme-variants.css‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
--color-beta: #1c1b1b;
1212
--color-gamma: #ffffff;
1313
--color-gamma-opaque: rgba(255, 255, 255, 0.3);
14+
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
15+
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
16+
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
17+
--ring-offset-shadow: 0 0 var(--color-beta);
18+
--ring-shadow: 0 0 var(--color-beta);
1419
}
1520

1621
/* Black Theme */
@@ -21,15 +26,26 @@
2126
--color-beta: #f2f2f2;
2227
--color-gamma: #1c1b1b;
2328
--color-gamma-opaque: rgba(28, 27, 27, 0.3);
29+
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
30+
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
31+
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
32+
--ring-offset-shadow: 0 0 var(--color-beta);
33+
--ring-shadow: 0 0 var(--color-beta);
2434
}
2535

2636
/* Gray Theme */
27-
.Theme--gray {
37+
.Theme--gray,
38+
.Theme--gray.dark {
2839
--color-border: #383735;
2940
--color-alpha: #ff8c2f;
3041
--color-beta: #383735;
3142
--color-gamma: #ffffff;
3243
--color-gamma-opaque: rgba(255, 255, 255, 0.3);
44+
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
45+
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
46+
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
47+
--ring-offset-shadow: 0 0 var(--color-beta);
48+
--ring-shadow: 0 0 var(--color-beta);
3349
}
3450

3551
/* Azure Theme */
@@ -39,6 +55,11 @@
3955
--color-beta: #e7f2f8;
4056
--color-gamma: #336699;
4157
--color-gamma-opaque: rgba(51, 102, 153, 0.3);
58+
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
59+
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
60+
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
61+
--ring-offset-shadow: 0 0 var(--color-beta);
62+
--ring-shadow: 0 0 var(--color-beta);
4263
}
4364

4465
/* Dark Mode Overrides */

‎api/generated-schema.graphql‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,23 @@ input UpdateApiKeyInput {
944944
permissions: [AddPermissionInput!]
945945
}
946946

947+
"""Customization related mutations"""
948+
type CustomizationMutations {
949+
"""Update the UI theme (writes dynamix.cfg)"""
950+
setTheme(
951+
"""Theme to apply"""
952+
theme: ThemeName!
953+
): Theme!
954+
}
955+
956+
"""The theme name"""
957+
enum ThemeName {
958+
azure
959+
black
960+
gray
961+
white
962+
}
963+
947964
"""
948965
Parity check related mutations, WIP, response types and functionaliy will change
949966
"""
@@ -1042,14 +1059,6 @@ type Theme {
10421059
headerSecondaryTextColor: String
10431060
}
10441061

1045-
"""The theme name"""
1046-
enum ThemeName {
1047-
azure
1048-
black
1049-
gray
1050-
white
1051-
}
1052-
10531062
type ExplicitStatusItem {
10541063
name: String!
10551064
updateStatus: UpdateStatus!
@@ -2449,6 +2458,7 @@ type Mutation {
24492458
vm: VmMutations!
24502459
parityCheck: ParityCheckMutations!
24512460
apiKey: ApiKeyMutations!
2461+
customization: CustomizationMutations!
24522462
rclone: RCloneMutations!
24532463
createDockerFolder(name: String!, parentId: String, childrenIds: [String!]): ResolvedOrganizerV1!
24542464
setDockerFolderChildren(folderId: String, childrenIds: [String!]!): ResolvedOrganizerV1!
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Module } from '@nestjs/common';
22

3+
import { CustomizationMutationsResolver } from '@app/unraid-api/graph/resolvers/customization/customization.mutations.resolver.js';
34
import { CustomizationResolver } from '@app/unraid-api/graph/resolvers/customization/customization.resolver.js';
45
import { CustomizationService } from '@app/unraid-api/graph/resolvers/customization/customization.service.js';
56

67
@Module({
7-
providers: [CustomizationService, CustomizationResolver],
8+
providers: [CustomizationService, CustomizationResolver, CustomizationMutationsResolver],
9+
exports: [CustomizationService],
810
})
911
export class CustomizationModule {}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
2+
3+
import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';
4+
import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';
5+
6+
import { CustomizationService } from '@app/unraid-api/graph/resolvers/customization/customization.service.js';
7+
import { Theme, ThemeName } from '@app/unraid-api/graph/resolvers/customization/theme.model.js';
8+
import { CustomizationMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
9+
10+
@Resolver(() => CustomizationMutations)
11+
export class CustomizationMutationsResolver {
12+
constructor(private readonly customizationService: CustomizationService) {}
13+
14+
@ResolveField(() => Theme, { description: 'Update the UI theme (writes dynamix.cfg)' })
15+
@UsePermissions({
16+
action: AuthAction.UPDATE_ANY,
17+
resource: Resource.CUSTOMIZATIONS,
18+
})
19+
async setTheme(
20+
@Args('theme', { type: () => ThemeName, description: 'Theme to apply' })
21+
theme: ThemeName
22+
): Promise<Theme> {
23+
return this.customizationService.setTheme(theme);
24+
}
25+
}

‎api/src/unraid-api/graph/resolvers/customization/customization.service.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import * as ini from 'ini';
99

1010
import { emcmd } from '@app/core/utils/clients/emcmd.js';
1111
import { fileExists } from '@app/core/utils/files/file-exists.js';
12+
import { loadDynamixConfigFromDiskSync } from '@app/store/actions/load-dynamix-config-file.js';
1213
import { getters, store } from '@app/store/index.js';
14+
import { updateDynamixConfig } from '@app/store/modules/dynamix.js';
1315
import {
1416
ActivationCode,
1517
PublicPartnerInfo,
@@ -466,4 +468,16 @@ export class CustomizationService implements OnModuleInit {
466468
showHeaderDescription: descriptionShow === 'yes',
467469
};
468470
}
471+
472+
public async setTheme(theme: ThemeName): Promise<Theme> {
473+
this.logger.log(`Updating theme to ${theme}`);
474+
await this.updateCfgFile(this.configFile, 'display', { theme });
475+
476+
// Refresh in-memory store so subsequent reads get the new theme without a restart
477+
const paths = getters.paths();
478+
const updatedConfig = loadDynamixConfigFromDiskSync(paths['dynamix-config']);
479+
store.dispatch(updateDynamixConfig(updatedConfig));
480+
481+
return this.getTheme();
482+
}
469483
}

‎api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export class VmMutations {}
2424
})
2525
export class ApiKeyMutations {}
2626

27+
@ObjectType({
28+
description: 'Customization related mutations',
29+
})
30+
export class CustomizationMutations {}
31+
2732
@ObjectType({
2833
description: 'Parity check related mutations, WIP, response types and functionaliy will change',
2934
})
@@ -54,6 +59,9 @@ export class RootMutations {
5459
@Field(() => ApiKeyMutations, { description: 'API Key related mutations' })
5560
apiKey: ApiKeyMutations = new ApiKeyMutations();
5661

62+
@Field(() => CustomizationMutations, { description: 'Customization related mutations' })
63+
customization: CustomizationMutations = new CustomizationMutations();
64+
5765
@Field(() => ParityCheckMutations, { description: 'Parity check related mutations' })
5866
parityCheck: ParityCheckMutations = new ParityCheckMutations();
5967

‎api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Mutation, Resolver } from '@nestjs/graphql';
33
import {
44
ApiKeyMutations,
55
ArrayMutations,
6+
CustomizationMutations,
67
DockerMutations,
78
ParityCheckMutations,
89
RCloneMutations,
@@ -37,6 +38,11 @@ export class RootMutationsResolver {
3738
return new ApiKeyMutations();
3839
}
3940

41+
@Mutation(() => CustomizationMutations, { name: 'customization' })
42+
customization(): CustomizationMutations {
43+
return new CustomizationMutations();
44+
}
45+
4046
@Mutation(() => RCloneMutations, { name: 'rclone' })
4147
rclone(): RCloneMutations {
4248
return new RCloneMutations();

‎plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php‎

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ private function sanitizeForId(string $input): string
4444
public function getManifestContents(string $manifestPath): array
4545
{
4646
$contents = @file_get_contents($manifestPath);
47-
return $contents ? json_decode($contents, true) : [];
47+
if (!$contents) {
48+
return [];
49+
}
50+
51+
$decoded = json_decode($contents, true);
52+
return is_array($decoded) ? $decoded : [];
4853
}
4954

5055
private function processManifestFiles(): string
@@ -209,6 +214,11 @@ private function getDisplayThemeVars(): ?array
209214
}
210215

211216
$theme = strtolower(trim($display['theme'] ?? ''));
217+
$darkThemes = ['gray', 'black'];
218+
$isDarkMode = in_array($theme, $darkThemes, true);
219+
$vars['--theme-dark-mode'] = $isDarkMode ? '1' : '0';
220+
$vars['--theme-name'] = $theme ?: 'white';
221+
212222
if ($theme === 'white') {
213223
if (!$textPrimary) {
214224
$vars['--header-text-primary'] = 'var(--inverse-text-color, #ffffff)';
@@ -218,19 +228,35 @@ private function getDisplayThemeVars(): ?array
218228
}
219229
}
220230

231+
// Unraid WebGUI stores banner enablement as a non-empty `display['banner']` value
232+
// (typically the banner file name/path).
233+
$shouldShowBanner = !empty($display['banner']);
221234
$bgColor = $this->normalizeHex($display['background'] ?? null);
222235
if ($bgColor) {
223236
$vars['--header-background-color'] = $bgColor;
224-
$vars['--header-gradient-start'] = $this->hexToRgba($bgColor, 0);
225-
$vars['--header-gradient-end'] = $this->hexToRgba($bgColor, 0.7);
237+
// Only set gradient variables if banner image is enabled
238+
if ($shouldShowBanner) {
239+
$vars['--header-gradient-start'] = $this->hexToRgba($bgColor, 0);
240+
$vars['--header-gradient-end'] = $this->hexToRgba($bgColor, 1);
241+
}
226242
}
227243

228244
$shouldShowBannerGradient = ($display['showBannerGradient'] ?? '') === 'yes';
229-
if ($shouldShowBannerGradient) {
230-
$start = $vars['--header-gradient-start'] ?? 'rgba(0, 0, 0, 0)';
231-
$end = $vars['--header-gradient-end'] ?? 'rgba(0, 0, 0, 0.7)';
245+
if ($shouldShowBanner && $shouldShowBannerGradient) {
246+
// If the user didn't set a custom background color, prefer existing theme defaults instead of falling back to black.
247+
if (!isset($vars['--header-gradient-start'])) {
248+
$vars['--header-gradient-start'] = 'var(--color-header-gradient-start, rgba(242, 242, 242, 0))';
249+
}
250+
if (!isset($vars['--header-gradient-end'])) {
251+
$vars['--header-gradient-end'] = 'var(--color-header-gradient-end, rgba(242, 242, 242, 1))';
252+
}
253+
$start = $vars['--header-gradient-start'];
254+
$end = $vars['--header-gradient-end'];
255+
// Keep compatibility with older CSS that expects these names.
256+
$vars['--color-header-gradient-start'] = $start;
257+
$vars['--color-header-gradient-end'] = $end;
232258
$vars['--banner-gradient'] = sprintf(
233-
'linear-gradient(90deg, %s 0, %s 90%%)',
259+
'linear-gradient(90deg, %s 0, %s var(--banner-gradient-stop, 30%%))',
234260
$start,
235261
$end
236262
);

0 commit comments

Comments
 (0)