You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by sh...@apache.org on 2024/01/16 02:38:47 UTC

(incubator-answer) 01/01: feat: add dark mode

This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch feat/1.2.5/ui
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git

commit 8e1e55e089522f2e1e5bc811601b8e92c88711a7
Author: shuai <li...@sifou.com>
AuthorDate: Tue Jan 16 10:38:20 2024 +0800

    feat: add dark mode
---
 i18n/en_US.yaml                                 | 11 +++--
 ui/public/index.html                            |  2 +-
 ui/src/common/color.scss                        | 52 ++++++++++++++++++++++++
 ui/src/common/constants.ts                      |  1 +
 ui/src/common/interface.ts                      |  1 +
 ui/src/components/Editor/index.scss             | 10 ++---
 ui/src/components/SideNav/index.scss            |  5 +--
 ui/src/index.scss                               | 17 ++++----
 ui/src/pages/Admin/Themes/index.tsx             | 19 ++++++++-
 ui/src/pages/Layout/index.tsx                   | 20 ++++++++++
 ui/src/pages/Questions/EditAnswer/index.scss    |  4 ++
 ui/src/pages/Questions/EditAnswer/index.tsx     |  2 +-
 ui/src/pages/Users/Settings/Interface/index.tsx | 53 +++++++++++++++++++++----
 ui/src/services/client/settings.ts              | 10 +++--
 ui/src/stores/loggedUserInfo.ts                 |  1 +
 ui/src/stores/themeSetting.ts                   |  2 +
 ui/src/utils/common.ts                          | 16 ++++++++
 ui/src/utils/guard.ts                           |  3 +-
 ui/src/utils/localize.ts                        | 26 +++++++++++-
 19 files changed, 220 insertions(+), 35 deletions(-)

diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index cd1e5b20..c96ed6ec 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1159,6 +1159,10 @@ ui:
     close: Close
     reopen: Reopen
     ok: OK
+    light: Light
+    dark: Dark
+    system_setting: System setting
+    default: Default
   search:
     title: Search Results
     keywords: Keywords
@@ -1687,11 +1691,12 @@ ui:
       themes:
         label: Themes
         text: Select an existing theme.
+      color_scheme:
+        label: Color scheme
       navbar_style:
-        label: Navbar Style
-        text: Select an existing theme.
+        label: Navbar style
       primary_color:
-        label: Primary Color
+        label: Primary color
         text: Modify the colors used by your themes
     css_and_html:
       page_title: CSS and HTML
diff --git a/ui/public/index.html b/ui/public/index.html
index eeb5fb11..f5a9c141 100644
--- a/ui/public/index.html
+++ b/ui/public/index.html
@@ -19,7 +19,7 @@
 
 -->
 <!DOCTYPE html>
-<html>
+<html data-bs-theme="">
   <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
diff --git a/ui/src/common/color.scss b/ui/src/common/color.scss
new file mode 100644
index 00000000..01d2d08f
--- /dev/null
+++ b/ui/src/common/color.scss
@@ -0,0 +1,52 @@
+
+
+:root {
+  --an-side-nav-link: rgba(0, 0, 0, 0.65);
+  --an-toolbar-divider: rgba(0, 0, 0, 0.1);
+  --an-ced4da: #ced4da;
+  --an-e9ecef: #e9ecef;
+  --an-pre: #161b22;
+  --an-6c757d: #6c757d;
+  --an-212529: #212529;
+  --an-gray-300: var(--bs-gray-300);
+  --an-white: #fff;
+  --an-inbox-warning:#fff3cd80;
+  --an-f5: #f5f5f5;
+}
+
+
+[data-bs-theme="dark"] {
+    --an-side-nav-link: rgba(255, 255, 255, 0.65);
+    --an-toolbar-divider: rgba(255, 255, 255, 0.3);
+    --an-ced4da: var(--bs-border-color);
+    --an-e9ecef: #161b22;
+    --an-pre: #161b22;
+    --an-6c757d: var(--bs-body-color);
+    --an-212529: var(--bs-body-color);
+    --an-gray-300: #161b22;
+    --an-white: #000;
+    --an-inbox-warning:#38363180;
+    --an-f5: var(--bs-body-bg);
+ }
+
+[data-bs-theme="dark"] {
+  .link-dark {
+    color: rgba(var(--bs-emphasis-color-rgb),0.8)!important;
+  }
+  /** editor **/
+
+  .CodeMirror {
+    background: var(--bs-body-bg);
+    color: var(--bs-body-color);
+  }
+  .CodeMirror-cursor {
+    border-left: 1px solid var(--bs-body-color);
+  }
+  .cm-header, .cm-link, .cm-url {
+    color: var(--bs-body-color);
+  }
+
+  .bg-light {
+    background-color: rgba(0, 0, 0, 0.5) !important;
+  }
+}
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index 87b2377b..275fe27d 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -28,6 +28,7 @@ export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_';
 export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_';
 export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|';
 export const QUESTIONS_ORDER_STORAGE_KEY = '_a_qok_';
+export const DEFAULT_THEME = 'system';
 
 export const USER_AGENT_NAMES = {
   SegmentFault: 'SegmentFault',
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 68f3be95..56bbc69f 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -440,6 +440,7 @@ export type themeConfig = {
 };
 export interface AdminSettingsTheme {
   theme: string;
+  color_scheme: string;
   theme_options?: { label: string; value: string }[];
   theme_config: Record<string, themeConfig>;
 }
diff --git a/ui/src/components/Editor/index.scss b/ui/src/components/Editor/index.scss
index e71d27fb..5b940e19 100644
--- a/ui/src/components/Editor/index.scss
+++ b/ui/src/components/Editor/index.scss
@@ -20,16 +20,16 @@
 .md-editor-wrap {
   display: flex;
   flex-direction: column;
-  background-color: #fff;
-  border: 1px solid #ced4da;
+  background-color: var(-bs-body-bg);
+  border: 1px solid var(--an-ced4da);
   overflow: hidden;
   .toolbar-wrap {
-    border-bottom: 1px solid #ced4da;
+    border-bottom: 1px solid var(--an-ced4da);
     .toolbar-divider {
       float: left;
       width: 1px;
       height: 15px;
-      background-color: rgba(0, 0, 0, 0.1);
+      background-color: var(--an-toolbar-divider);
       margin: 10px 8px;
     }
     .toolbar-item-wrap {
@@ -57,7 +57,7 @@
       box-sizing: border-box;
       outline: none;
       cursor: pointer;
-      background-color: #fff;
+      background-color: var(--bs-body-bg);
       height: 100%;
       width: 100%;
       border-radius: 0;
diff --git a/ui/src/components/SideNav/index.scss b/ui/src/components/SideNav/index.scss
index 802dbcd1..c987591d 100644
--- a/ui/src/components/SideNav/index.scss
+++ b/ui/src/components/SideNav/index.scss
@@ -17,7 +17,6 @@
  * under the License.
  */
 
-
 #sideNav {
   .nav-wrap {
     position: sticky;
@@ -32,10 +31,10 @@
     max-width: calc(100% - 24px);
   }
   .nav-link {
-    color: rgba(0, 0, 0, 0.65);
+    color: var(--an-side-nav-link);
   }
   .nav-link:hover {
-    color: rgba(0, 0, 0);
+    color: black;
     background-color: var(--bs-gray-100);
   }
   .nav-link.active {
diff --git a/ui/src/index.scss b/ui/src/index.scss
index e9535748..0c618e80 100644
--- a/ui/src/index.scss
+++ b/ui/src/index.scss
@@ -20,9 +20,10 @@
 @import 'common/variable';
 @import '~bootstrap/scss/bootstrap';
 @import '~bootstrap-icons';
+@import 'common/color';
 
 .bg-gray-300 {
-  background-color: $gray-300;
+  background-color: var(--an-gray-300);
 }
 
 .focus {
@@ -132,7 +133,7 @@ img[src=""] {
 }
 
 .bg-f5 {
-  background-color: #f5f5f5;
+  background-color: var(--an-f5);
 }
 
 .btn-no-border,
@@ -198,7 +199,7 @@ img[src=""] {
 }
 
 .warning {
-  background-color: #fff3cd80;
+  background-color: var(--an-inbox-warning);
 }
 
 .fit-content {
@@ -246,14 +247,14 @@ img[src=""] {
   }
   p {
     > code {
-      background-color: #e9ecef;
-      color: #212529;
+      background-color: var(--an-e9ecef);
+      color: var(--an-212529);
       padding: 2px 4px;
       border-radius: 0.25rem;
     }
   }
   pre {
-    background-color: #e9ecef;
+    background-color: var(--an-e9ecef);
     border-radius: 0.25rem;
     padding: 1rem;
   }
@@ -261,9 +262,9 @@ img[src=""] {
     border-left: 0.25rem solid #ced4da;
     padding: 1rem;
     color: #6c757d;
-    background-color: #e9ecef;
+    background-color: var(--an-e9ecef);
     p {
-      color: $body-color;
+      color: var(--bs-body-color);
     }
     > p:last-child {
       margin-bottom: 0;
diff --git a/ui/src/pages/Admin/Themes/index.tsx b/ui/src/pages/Admin/Themes/index.tsx
index 34cb42d9..3f817712 100644
--- a/ui/src/pages/Admin/Themes/index.tsx
+++ b/ui/src/pages/Admin/Themes/index.tsx
@@ -26,6 +26,7 @@ import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
 import { useToast } from '@/hooks';
 import { handleFormError } from '@/utils';
 import { themeSettingStore } from '@/stores';
+import { setupAppTheme } from '@/utils/localize';
 
 const Index: FC = () => {
   const { t } = useTranslation('translation', {
@@ -44,10 +45,20 @@ const Index: FC = () => {
         enumNames: themeSetting?.theme_options?.map((_) => _.label),
         default: themeSetting?.theme_options?.[0]?.value,
       },
+      color_scheme: {
+        type: 'string',
+        title: t('color_scheme.label'),
+        enum: ['setting', 'light', 'dark'],
+        enumNames: [
+          t('system_setting', { keyPrefix: 'btns' }),
+          t('light', { keyPrefix: 'btns' }),
+          t('dark', { keyPrefix: 'btns' }),
+        ],
+        default: themeSetting?.color_scheme,
+      },
       navbar_style: {
         type: 'string',
         title: t('navbar_style.label'),
-        description: t('navbar_style.text'),
         enum: ['colored', 'light'],
         enumNames: ['Colored', 'Light'],
         default: 'colored',
@@ -64,6 +75,9 @@ const Index: FC = () => {
     themes: {
       'ui:widget': 'select',
     },
+    color_scheme: {
+      'ui:widget': 'select',
+    },
     navbar_style: {
       'ui:widget': 'select',
     },
@@ -81,6 +95,7 @@ const Index: FC = () => {
     const themeName = formData.themes.value;
     const reqParams: Type.AdminSettingsTheme = {
       theme: themeName,
+      color_scheme: formData.color_scheme.value,
       theme_config: {
         [themeName]: {
           navbar_style: formData.navbar_style.value,
@@ -96,6 +111,7 @@ const Index: FC = () => {
           variant: 'success',
         });
         updateThemeSetting(reqParams);
+        setupAppTheme();
       })
       .catch((err) => {
         if (err.isError) {
@@ -115,6 +131,7 @@ const Index: FC = () => {
         formMeta.themes.value = themeName;
         formMeta.navbar_style.value = themeConfig?.navbar_style;
         formMeta.primary_color.value = themeConfig?.primary_color;
+        formData.color_scheme.value = setting?.color_scheme || 'system';
         setFormData({ ...formMeta });
       }
     });
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx
index 1be44d9d..eb44e3e3 100644
--- a/ui/src/pages/Layout/index.tsx
+++ b/ui/src/pages/Layout/index.tsx
@@ -34,6 +34,7 @@ import {
   HttpErrorContent,
 } from '@/components';
 import { LoginToContinueModal } from '@/components/Modal';
+import { changeTheme } from '@/utils';
 
 const Layout: FC = () => {
   const location = useLocation();
@@ -47,6 +48,25 @@ const Layout: FC = () => {
   useEffect(() => {
     httpStatusReset();
   }, [location]);
+
+  useEffect(() => {
+    const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
+    function handleSystemThemeChange(event) {
+      if (event.matches) {
+        console.log('system theme is dark');
+        changeTheme('dark');
+      } else {
+        console.log('system theme is light');
+        changeTheme('light');
+      }
+    }
+
+    systemThemeQuery.addListener(handleSystemThemeChange);
+
+    return () => {
+      systemThemeQuery.removeListener(handleSystemThemeChange);
+    };
+  }, []);
   return (
     <HelmetProvider>
       <PageTags />
diff --git a/ui/src/pages/Questions/EditAnswer/index.scss b/ui/src/pages/Questions/EditAnswer/index.scss
index 9bae8ebe..68dfb023 100644
--- a/ui/src/pages/Questions/EditAnswer/index.scss
+++ b/ui/src/pages/Questions/EditAnswer/index.scss
@@ -27,6 +27,7 @@
       height: calc(100% - 12px);
       overflow-y: scroll;
       z-index: 1;
+      background: var(--an-white);
     }
     .scroll-bar {
       height: 12px;
@@ -46,6 +47,9 @@
       transform: scale(110, 1);
       height: 100%;
     }
+    .resize-bottom::-webkit-resizer {
+      border: none;
+    }
     .resize-bottom + .line {
       left: 0;
       width: 100%;
diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx
index d693794d..9f4d9d65 100644
--- a/ui/src/pages/Questions/EditAnswer/index.tsx
+++ b/ui/src/pages/Questions/EditAnswer/index.tsx
@@ -227,7 +227,7 @@ const Index = () => {
           <div className="question-content-wrap">
             <div
               ref={questionContentRef}
-              className="content position-absolute top-0 w-100 bg-white"
+              className="content position-absolute top-0 w-100"
               dangerouslySetInnerHTML={{ __html: data?.question.html }}
             />
             <div
diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx
index 4164394f..db91acf1 100644
--- a/ui/src/pages/Users/Settings/Interface/index.tsx
+++ b/ui/src/pages/Users/Settings/Interface/index.tsx
@@ -25,7 +25,7 @@ import { useToast } from '@/hooks';
 import { updateUserInterface } from '@/services';
 import { localize } from '@/utils';
 import { loggedUserInfoStore } from '@/stores';
-import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
+import { SchemaForm, JSONSchema, UISchema } from '@/components';
 
 const Index = () => {
   const { t } = useTranslation('translation', {
@@ -34,10 +34,22 @@ const Index = () => {
   const loggedUserInfo = loggedUserInfoStore.getState().user;
   const toast = useToast();
   const [langs, setLangs] = useState<LangsType[]>();
+  const [formData, setFormData] = useState<FormDataType>({
+    language: {
+      value: loggedUserInfo.language,
+      isInvalid: false,
+      errorMsg: '',
+    },
+    color_scheme: {
+      value: loggedUserInfo.color_scheme || 'default',
+      isInvalid: false,
+      errorMsg: '',
+    },
+  });
   const schema: JSONSchema = {
     title: t('heading'),
     properties: {
-      lang: {
+      language: {
         type: 'string',
         title: t('lang.label'),
         description: t('lang.text'),
@@ -45,32 +57,59 @@ const Index = () => {
         enumNames: langs?.map((_) => _.label),
         default: loggedUserInfo.language,
       },
+      color_scheme: {
+        type: 'string',
+        title: t('color_scheme.label', { keyPrefix: 'admin.themes' }),
+        enum: ['default', 'system', 'light', 'dark'],
+        enumNames: [
+          t('default', { keyPrefix: 'btns' }),
+          t('system_setting', { keyPrefix: 'btns' }),
+          t('light', { keyPrefix: 'btns' }),
+          t('dark', { keyPrefix: 'btns' }),
+        ],
+        default: loggedUserInfo.color_scheme,
+      },
     },
   };
   const uiSchema: UISchema = {
-    lang: {
+    language: {
+      'ui:widget': 'select',
+    },
+    color_scheme: {
       'ui:widget': 'select',
     },
   };
-  const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
 
   const getLangs = async () => {
     const res: LangsType[] = await localize.loadLanguageOptions();
+    setFormData({
+      ...formData,
+      language: {
+        ...formData.language,
+        value: res[0].value,
+      },
+    });
     setLangs(res);
   };
 
+  console.log(formData.color_scheme, '3333');
+
   const handleOnChange = (d) => {
     setFormData(d);
   };
   const handleSubmit = (event: FormEvent) => {
     event.preventDefault();
-    const lang = formData.lang.value;
-    updateUserInterface(lang).then(() => {
+    const params = {
+      language: formData.language.value,
+      color_scheme: formData.color_scheme.value,
+    };
+    updateUserInterface(params).then(() => {
       loggedUserInfoStore.getState().update({
         ...loggedUserInfo,
-        language: lang,
+        ...params,
       });
       localize.setupAppLanguage();
+      localize.setupAppTheme();
       toast.onShow({
         msg: t('update', { keyPrefix: 'toast' }),
         variant: 'success',
diff --git a/ui/src/services/client/settings.ts b/ui/src/services/client/settings.ts
index 51b3a6a1..05cea96c 100644
--- a/ui/src/services/client/settings.ts
+++ b/ui/src/services/client/settings.ts
@@ -30,10 +30,12 @@ export const getLanguageOptions = () => {
   return request.get<Type.LangsType[]>('/answer/api/v1/language/options');
 };
 
-export const updateUserInterface = (lang: string) => {
-  return request.put('/answer/api/v1/user/interface', {
-    language: lang,
-  });
+interface userSettingInterface {
+  language: '';
+  color_scheme: '';
+}
+export const updateUserInterface = (data: userSettingInterface) => {
+  return request.put('/answer/api/v1/user/interface', data);
 };
 
 export const useGetNotificationConfig = () => {
diff --git a/ui/src/stores/loggedUserInfo.ts b/ui/src/stores/loggedUserInfo.ts
index e1d5e4af..1157e631 100644
--- a/ui/src/stores/loggedUserInfo.ts
+++ b/ui/src/stores/loggedUserInfo.ts
@@ -42,6 +42,7 @@ const initUser: UserInfoRes = {
   status: 'normal',
   mail_status: 1,
   language: 'Default',
+  color_scheme: 'default',
   is_admin: false,
   have_password: true,
   role_id: 1,
diff --git a/ui/src/stores/themeSetting.ts b/ui/src/stores/themeSetting.ts
index 1100a027..d0788cc8 100644
--- a/ui/src/stores/themeSetting.ts
+++ b/ui/src/stores/themeSetting.ts
@@ -25,11 +25,13 @@ interface IType {
   theme: AdminSettingsTheme['theme'];
   theme_config: AdminSettingsTheme['theme_config'];
   theme_options: AdminSettingsTheme['theme_options'];
+  color_scheme: AdminSettingsTheme['color_scheme'];
   update: (params: AdminSettingsTheme) => void;
 }
 
 const store = create<IType>((set) => ({
   theme: 'default',
+  color_scheme: 'system',
   theme_options: [{ label: 'Default', value: 'default' }],
   theme_config: {
     default: {
diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts
index b6382a4e..22452019 100644
--- a/ui/src/utils/common.ts
+++ b/ui/src/utils/common.ts
@@ -246,6 +246,21 @@ function getUaType() {
   return null;
 }
 
+function changeTheme(mode: '' | 'light' | 'dark' | 'system') {
+  const htmlTag = document.querySelector('html') as HTMLHtmlElement;
+  if (mode === 'system') {
+    const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
+
+    if (systemThemeQuery.matches) {
+      htmlTag.setAttribute('data-bs-theme', 'dark');
+    } else {
+      htmlTag.setAttribute('data-bs-theme', 'light');
+    }
+  } else {
+    htmlTag.setAttribute('data-bs-theme', mode);
+  }
+}
+
 export {
   thousandthDivision,
   formatCount,
@@ -260,4 +275,5 @@ export {
   diffText,
   base64ToSvg,
   getUaType,
+  changeTheme,
 };
diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts
index b424b87f..3abf1054 100644
--- a/ui/src/utils/guard.ts
+++ b/ui/src/utils/guard.ts
@@ -38,7 +38,7 @@ import {
 } from '@/common/constants';
 import Storage from '@/utils/storage';
 
-import { setupAppLanguage, setupAppTimeZone } from './localize';
+import { setupAppLanguage, setupAppTimeZone, setupAppTheme } from './localize';
 import { floppyNavigation, NavigateConfig } from './floppyNavigation';
 import { pullUcAgent, getSignUpUrl } from './userCenter';
 
@@ -453,6 +453,7 @@ export const setupApp = async () => {
   await Promise.allSettled([pullUcAgent()]);
   setupAppLanguage();
   setupAppTimeZone();
+  setupAppTheme();
   /**
    * WARN:
    * Initialization must be completed after all initialization actions,
diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts
index 760b7967..850a1a2c 100644
--- a/ui/src/utils/localize.ts
+++ b/ui/src/utils/localize.ts
@@ -22,17 +22,23 @@ import i18next from 'i18next';
 import utc from 'dayjs/plugin/utc';
 import timezone from 'dayjs/plugin/timezone';
 
-import { interfaceStore, loggedUserInfoStore } from '@/stores';
+import {
+  interfaceStore,
+  loggedUserInfoStore,
+  themeSettingStore,
+} from '@/stores';
 import {
   CURRENT_LANG_STORAGE_KEY,
   DEFAULT_LANG,
   LANG_RESOURCE_STORAGE_KEY,
+  DEFAULT_THEME,
 } from '@/common/constants';
 import {
   getAdminLanguageOptions,
   getLanguageConfig,
   getLanguageOptions,
 } from '@/services';
+import { changeTheme } from '@/utils/common';
 
 import Storage from './storage';
 
@@ -103,6 +109,18 @@ export const getCurrentLang = () => {
   return currentLang;
 };
 
+export const getCurrentTheme = () => {
+  const loggedUser = loggedUserInfoStore.getState().user;
+  const adminTheme = themeSettingStore.getState().color_scheme;
+  const fallbackTheme = DEFAULT_THEME;
+  let currentTheme = loggedUser.color_scheme;
+  if (/default/i.test(currentTheme)) {
+    currentTheme = adminTheme;
+  }
+  currentTheme ||= fallbackTheme;
+  return currentTheme;
+};
+
 /**
  * localize for Day.js
  */
@@ -128,3 +146,9 @@ export const setupAppTimeZone = () => {
     dayjs.tz.setDefault(adminInterface.time_zone);
   }
 };
+
+export const setupAppTheme = () => {
+  const theme = getCurrentTheme();
+  console.log(333, theme);
+  changeTheme(theme);
+};