feat(settings): add settings to adjust screen brightness when reading, closes #1532 (#2197)

This commit is contained in:
Huang Xin
2025-10-11 01:02:35 +08:00
committed by GitHub
parent f696b9a573
commit ada427b134
54 changed files with 588 additions and 57 deletions
@@ -615,5 +615,6 @@
"File count: {{size}}": "عدد الملفات: {{size}}",
"Background Read Aloud": "قراءة بصوت عالٍ في الخلفية",
"Ready to read aloud": "جاهز للقراءة بصوت عالٍ",
"Read Aloud": "القراءة بصوت عالٍ"
"Read Aloud": "القراءة بصوت عالٍ",
"Screen Brightness": "سطوع الشاشة"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "ফাইলের সংখ্যা: {{size}}",
"Background Read Aloud": "পটভূমিতে উচ্চস্বরে পড়ুন",
"Ready to read aloud": "উচ্চস্বরে পড়ার জন্য প্রস্তুত",
"Read Aloud": "উচ্চস্বরে পড়ুন"
"Read Aloud": "উচ্চস্বরে পড়ুন",
"Screen Brightness": "স্ক্রিন উজ্জ্বলতা"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "དེབ་གནས་བཅས་པའི་ཨང་། {{size}}",
"Background Read Aloud": "གནས་སྟངས་ཀྱིས་ཀློག་བྱེད།",
"Ready to read aloud": "དེབ་ཀྱི་སྤྱོད་བྱས་མ་ཐུབ།",
"Read Aloud": "ཀློག་བྱེད།"
"Read Aloud": "ཀློག་བྱེད།",
"Screen Brightness": "རྒྱབ་སྐོར་གྱི་འོད་ཟེར།"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "Dateianzahl: {{size}}",
"Background Read Aloud": "Hintergrund-Vorlesen",
"Ready to read aloud": "Bereit zum Vorlesen",
"Read Aloud": "Vorlesen"
"Read Aloud": "Vorlesen",
"Screen Brightness": "Bildschirmhelligkeit"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "Αριθμός αρχείων: {{size}}",
"Background Read Aloud": "Ανάγνωση στο παρασκήνιο",
"Ready to read aloud": "Έτοιμο για ανάγνωση",
"Read Aloud": "Ανάγνωση"
"Read Aloud": "Ανάγνωση",
"Screen Brightness": "Φωτεινότητα οθόνης"
}
@@ -603,5 +603,6 @@
"File count: {{size}}": "Número de archivos: {{size}}",
"Background Read Aloud": "Lectura en segundo plano",
"Ready to read aloud": "Listo para leer en voz alta",
"Read Aloud": "Leer en voz alta"
"Read Aloud": "Leer en voz alta",
"Screen Brightness": "Brillo de pantalla"
}
@@ -603,5 +603,6 @@
"File count: {{size}}": "Nombre de fichiers : {{size}}",
"Background Read Aloud": "Lecture Vocale en Arrière-Plan",
"Ready to read aloud": "Prêt à lire à haute voix",
"Read Aloud": "Lire à haute voix"
"Read Aloud": "Lire à haute voix",
"Screen Brightness": "Luminosité de l'Écran"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "फ़ाइलों की संख्या: {{size}}",
"Background Read Aloud": "पृष्ठभूमि में वाचन",
"Ready to read aloud": "उच्च स्वर में पढ़ने के लिए तैयार",
"Read Aloud": "उच्च स्वर में पढ़ें"
"Read Aloud": "उच्च स्वर में पढ़ें",
"Screen Brightness": "स्क्रीन ब्राइटनेस"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "Jumlah file: {{size}}",
"Background Read Aloud": "Bacakan Latar Belakang",
"Ready to read aloud": "Siap untuk dibacakan",
"Read Aloud": "Bacakan"
"Read Aloud": "Bacakan",
"Screen Brightness": "Kecerahan Layar"
}
@@ -603,5 +603,6 @@
"File count: {{size}}": "Conteggio file: {{size}}",
"Background Read Aloud": "Riproduzione in background",
"Ready to read aloud": "Pronto per la lettura ad alta voce",
"Read Aloud": "Leggi ad alta voce"
"Read Aloud": "Leggi ad alta voce",
"Screen Brightness": "Luminosità dello schermo"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "ファイル数: {{size}}",
"Background Read Aloud": "バックグラウンド読み上げ",
"Ready to read aloud": "読み上げの準備ができました",
"Read Aloud": "読み上げ"
"Read Aloud": "読み上げ",
"Screen Brightness": "画面の明るさ"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "파일 수: {{size}}",
"Background Read Aloud": "백그라운드 음성 읽기",
"Ready to read aloud": "읽기 준비 완료",
"Read Aloud": "음성 읽기"
"Read Aloud": "음성 읽기",
"Screen Brightness": "화면 밝기"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "Bestandsaantal: {{size}}",
"Background Read Aloud": "Achtergrond Voorlezen",
"Ready to read aloud": "Klaar om voor te lezen",
"Read Aloud": "Voorlezen"
"Read Aloud": "Voorlezen",
"Screen Brightness": "Schermhelderheid"
}
@@ -607,5 +607,6 @@
"File count: {{size}}": "Liczba plików: {{size}}",
"Background Read Aloud": "Czytanie na głos w tle",
"Ready to read aloud": "Gotowy do przeczytania na głos",
"Read Aloud": "Czytaj na głos"
"Read Aloud": "Czytaj na głos",
"Screen Brightness": "Jasność ekranu"
}
@@ -603,5 +603,6 @@
"File count: {{size}}": "Contagem de arquivos: {{size}}",
"Background Read Aloud": "Leitura em Segundo Plano",
"Ready to read aloud": "Pronto para leitura em voz alta",
"Read Aloud": "Ler em Voz Alta"
"Read Aloud": "Ler em Voz Alta",
"Screen Brightness": "Brilho da Tela"
}
@@ -607,5 +607,6 @@
"File count: {{size}}": "Количество файлов: {{size}}",
"Background Read Aloud": "Озвучивание в фоне",
"Ready to read aloud": "Готов к чтению вслух",
"Read Aloud": "Читать вслух"
"Read Aloud": "Читать вслух",
"Screen Brightness": "Яркость экрана"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "ගොනු ගණන: {{size}}",
"Background Read Aloud": "පසුබිම් පවසා කියවීම",
"Ready to read aloud": "වාදනය කිරීමට සූදානම්",
"Read Aloud": "පවසා කියවීම"
"Read Aloud": "පවසා කියවීම",
"Screen Brightness": "තිරයේ දිදුලනුම"
}
@@ -599,5 +599,6 @@
"Sunset": "Solnedgång",
"Reveal in Finder": "Visa i Finder",
"Reveal in File Explorer": "Visa i Utforskaren",
"Reveal in Folder": "Visa i mapp"
"Reveal in Folder": "Visa i mapp",
"Screen Brightness": "Skärmens ljusstyrka"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "கோப்பு எண்ணிக்கை: {{size}}",
"Background Read Aloud": "பின்னணி உரை வாசிப்பு",
"Ready to read aloud": "வாசிக்க தயாராக உள்ளது",
"Read Aloud": "உரை வாசிப்பு"
"Read Aloud": "உரை வாசிப்பு",
"Screen Brightness": "திரை பிரகாசம்"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "จำนวนไฟล์: {{size}}",
"Background Read Aloud": "อ่านออกเสียงเบื้องหลัง",
"Ready to read aloud": "พร้อมที่จะอ่านออกเสียง",
"Read Aloud": "อ่านออกเสียง"
"Read Aloud": "อ่านออกเสียง",
"Screen Brightness": "ความสว่างหน้าจอ"
}
@@ -599,5 +599,6 @@
"File count: {{size}}": "Dosya sayısı: {{size}}",
"Background Read Aloud": "Arka Planda Sesli Okuma",
"Ready to read aloud": "Sesli okumaya hazır",
"Read Aloud": "Sesli Oku"
"Read Aloud": "Sesli Oku",
"Screen Brightness": "Ekran Parlaklığı"
}
@@ -607,5 +607,6 @@
"File count: {{size}}": "Кількість файлів: {{size}}",
"Background Read Aloud": "Озвучування у фоновому режимі",
"Ready to read aloud": "Готовий до читання вголос",
"Read Aloud": "Читати вголос"
"Read Aloud": "Читати вголос",
"Screen Brightness": "Яскравість екрану"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "Số lượng tệp: {{size}}",
"Background Read Aloud": "Đọc To Nền",
"Ready to read aloud": "Sẵn sàng để đọc to",
"Read Aloud": "Đọc To"
"Read Aloud": "Đọc To",
"Screen Brightness": "Độ sáng màn hình"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "文件数量: {{size}}",
"Background Read Aloud": "后台语音朗读",
"Ready to read aloud": "语音朗读已就绪",
"Read Aloud": "语音朗读"
"Read Aloud": "语音朗读",
"Screen Brightness": "屏幕亮度"
}
@@ -595,5 +595,6 @@
"File count: {{size}}": "文件數量: {{size}}",
"Background Read Aloud": "背景朗讀",
"Ready to read aloud": "朗讀準備就緒",
"Read Aloud": "朗讀"
"Read Aloud": "朗讀",
"Screen Brightness": "螢幕亮度"
}
@@ -69,6 +69,11 @@ class LockScreenOrientationRequestArgs {
var orientation: String? = null
}
@InvokeArg
class SetScreenBrightnessRequestArgs {
var brightness: Double? = null // 0.0 to 1.0
}
interface KeyDownInterceptor {
fun interceptVolumeKeys(enabled: Boolean)
fun interceptBackKey(enabled: Boolean)
@@ -76,7 +81,7 @@ interface KeyDownInterceptor {
@TauriPlugin(
permissions = [
Permission(strings = [Manifest.permission.MANAGE_EXTERNAL_STORAGE], alias = "manageStorage")
Permission(strings = [Manifest.permission.MANAGE_EXTERNAL_STORAGE], alias = "manageStorage"),
]
)
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
@@ -415,6 +420,44 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
invoke.resolve(ret)
}
@Command
fun get_screen_brightness(invoke: Invoke) {
val ret = JSObject()
try {
val brightness = Settings.System.getInt(
activity.contentResolver,
Settings.System.SCREEN_BRIGHTNESS
)
val normalizedBrightness = brightness / 255.0
ret.put("brightness", normalizedBrightness)
} catch (e: Exception) {
ret.put("error", e.message)
ret.put("brightness", -1.0)
}
invoke.resolve(ret)
}
@Command
fun set_screen_brightness(invoke: Invoke) {
val args = invoke.parseArgs(SetScreenBrightnessRequestArgs::class.java)
val ret = JSObject()
try {
val brightness = (args.brightness ?: 0.5).toFloat()
if (brightness < 0.0 || brightness > 1.0) {
invoke.reject("Brightness must be between 0.0 and 1.0")
return
}
val layoutParams = activity.window.attributes
layoutParams.screenBrightness = brightness
activity.window.attributes = layoutParams
ret.put("success", true)
} catch (e: Exception) {
ret.put("success", false)
ret.put("error", e.message)
}
invoke.resolve(ret)
}
@Command
fun request_manage_storage_permission(invoke: Invoke) {
val ret = JSObject()
@@ -15,6 +15,8 @@ const COMMANDS: &[&str] = &[
"iap_restore_purchases",
"get_system_color_scheme",
"get_safe_area_insets",
"get_screen_brightness",
"set_screen_brightness",
"request_manage_storage_permission",
"checkPermissions",
"requestPermissions",
@@ -49,6 +49,10 @@ class LockScreenOrientationRequestArgs: Decodable {
let orientation: String?
}
class SetScreenBrightnessRequestArgs: Decodable {
let brightness: Float?
}
struct InitializeRequest: Decodable {
let publicKey: String?
}
@@ -539,6 +543,28 @@ class NativeBridgePlugin: Plugin {
let colorScheme = (userInterfaceStyle == .dark) ? "dark" : "light"
invoke.resolve(["colorScheme": colorScheme])
}
@objc public func get_screen_brightness(_ invoke: Invoke) {
let brightness = UIScreen.main.brightness
invoke.resolve(["brightness": brightness])
}
@objc public func set_screen_brightness(_ invoke: Invoke) {
guard let args = try? invoke.parseArgs(SetScreenBrightnessRequestArgs.self) else {
return invoke.reject("Failed to parse arguments")
}
let brightness = args.brightness ?? 0.5
if brightness < 0.0 || brightness > 1.0 {
return invoke.reject("Brightness must be between 0.0 and 1.0")
}
DispatchQueue.main.async {
UIScreen.main.brightness = CGFloat(brightness)
}
invoke.resolve(["success": true])
}
}
@_cdecl("init_plugin_native_bridge")
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-screen-brightness"
description = "Enables the get_screen_brightness command without any pre-configured scope."
commands.allow = ["get_screen_brightness"]
[[permission]]
identifier = "deny-get-screen-brightness"
description = "Denies the get_screen_brightness command without any pre-configured scope."
commands.deny = ["get_screen_brightness"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-set-screen-brightness"
description = "Enables the set_screen_brightness command without any pre-configured scope."
commands.allow = ["set_screen_brightness"]
[[permission]]
identifier = "deny-set-screen-brightness"
description = "Denies the set_screen_brightness command without any pre-configured scope."
commands.deny = ["set_screen_brightness"]
@@ -20,6 +20,8 @@ Default permissions for the plugin
- `allow-iap-restore-purchases`
- `allow-get-system-color-scheme`
- `allow-get-safe-area-insets`
- `allow-get-screen-brightness`
- `allow-set-screen-brightness`
- `allow-request-manage-storage-permission`
- `allow-checkPermissions`
- `allow-requestPermissions`
@@ -166,6 +168,32 @@ Denies the get_safe_area_insets command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-get-screen-brightness`
</td>
<td>
Enables the get_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-get-screen-brightness`
</td>
<td>
Denies the get_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-get-status-bar-height`
</td>
@@ -478,6 +506,32 @@ Denies the request_manage_storage_permission command without any pre-configured
<tr>
<td>
`native-bridge:allow-set-screen-brightness`
</td>
<td>
Enables the set_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-set-screen-brightness`
</td>
<td>
Denies the set_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-set-system-ui-visibility`
</td>
@@ -17,6 +17,8 @@ permissions = [
"allow-iap-restore-purchases",
"allow-get-system-color-scheme",
"allow-get-safe-area-insets",
"allow-get-screen-brightness",
"allow-set-screen-brightness",
"allow-request-manage-storage-permission",
"allow-checkPermissions",
"allow-requestPermissions",
@@ -354,6 +354,18 @@
"const": "deny-get-safe-area-insets",
"markdownDescription": "Denies the get_safe_area_insets command without any pre-configured scope."
},
{
"description": "Enables the get_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "allow-get-screen-brightness",
"markdownDescription": "Enables the get_screen_brightness command without any pre-configured scope."
},
{
"description": "Denies the get_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "deny-get-screen-brightness",
"markdownDescription": "Denies the get_screen_brightness command without any pre-configured scope."
},
{
"description": "Enables the get_status_bar_height command without any pre-configured scope.",
"type": "string",
@@ -498,6 +510,18 @@
"const": "deny-request-manage-storage-permission",
"markdownDescription": "Denies the request_manage_storage_permission command without any pre-configured scope."
},
{
"description": "Enables the set_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "allow-set-screen-brightness",
"markdownDescription": "Enables the set_screen_brightness command without any pre-configured scope."
},
{
"description": "Denies the set_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "deny-set-screen-brightness",
"markdownDescription": "Denies the set_screen_brightness command without any pre-configured scope."
},
{
"description": "Enables the set_system_ui_visibility command without any pre-configured scope.",
"type": "string",
@@ -523,10 +547,10 @@
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"type": "string",
"const": "default",
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
}
]
}
@@ -127,6 +127,21 @@ pub(crate) async fn get_safe_area_insets<R: Runtime>(
app.native_bridge().get_safe_area_insets()
}
#[command]
pub(crate) async fn get_screen_brightness<R: Runtime>(
app: AppHandle<R>,
) -> Result<GetScreenBrightnessResponse> {
app.native_bridge().get_screen_brightness()
}
#[command]
pub(crate) async fn set_screen_brightness<R: Runtime>(
app: AppHandle<R>,
payload: SetScreenBrightnessRequest,
) -> Result<SetScreenBrightnessResponse> {
app.native_bridge().set_screen_brightness(payload)
}
#[command]
pub(crate) async fn request_manage_storage_permission<R: Runtime>(
app: AppHandle<R>,
@@ -107,6 +107,17 @@ impl<R: Runtime> NativeBridge<R> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn get_screen_brightness(&self) -> crate::Result<GetScreenBrightnessResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn set_screen_brightness(
&self,
_payload: SetScreenBrightnessRequest,
) -> crate::Result<SetScreenBrightnessResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn request_manage_storage_permission(
&self,
) -> crate::Result<RequestManageStoragePermissionResponse> {
@@ -53,6 +53,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::iap_restore_purchases,
commands::get_system_color_scheme,
commands::get_safe_area_insets,
commands::get_screen_brightness,
commands::set_screen_brightness,
commands::request_manage_storage_permission,
])
.setup(|app, api| {
@@ -170,6 +170,25 @@ impl<R: Runtime> NativeBridge<R> {
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn get_screen_brightness(&self) -> crate::Result<GetScreenBrightnessResponse> {
self.0
.run_mobile_plugin("get_screen_brightness", ())
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn set_screen_brightness(
&self,
payload: SetScreenBrightnessRequest,
) -> crate::Result<SetScreenBrightnessResponse> {
self.0
.run_mobile_plugin("set_screen_brightness", payload)
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn request_manage_storage_permission(
&self,
@@ -168,6 +168,25 @@ pub struct GetSafeAreaInsetsResponse {
pub right: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetScreenBrightnessResponse {
pub brightness: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetScreenBrightnessRequest {
pub brightness: f64, // 0.0 to 1.0
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetScreenBrightnessResponse {
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestManageStoragePermissionResponse {
@@ -0,0 +1,157 @@
import clsx from 'clsx';
import React, { useCallback, useEffect, useMemo } from 'react';
import { PiSun, PiMoon } from 'react-icons/pi';
import { TbSunMoon } from 'react-icons/tb';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useDeviceControlStore } from '@/store/deviceStore';
import { themes } from '@/styles/themes';
import { debounce } from '@/utils/debounce';
import Slider from '@/components/Slider';
const SCREEN_BRIGHTNESS_LIMITS = {
MIN: 0,
MAX: 100,
DEFAULT: 50,
} as const;
interface ColorPanelProps {
actionTab: string;
bottomOffset: string;
}
export const ColorPanel: React.FC<ColorPanelProps> = ({ actionTab, bottomOffset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { getScreenBrightness, setScreenBrightness } = useDeviceControlStore();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor } = useThemeStore();
const [screenBrightnessValue, setScreenBrightnessValue] = React.useState(
settings.screenBrightness >= 0 ? settings.screenBrightness : SCREEN_BRIGHTNESS_LIMITS.DEFAULT,
);
useEffect(() => {
if (!appService?.isMobileApp) return;
if (settings.screenBrightness >= 0) return;
getScreenBrightness().then((brightness) => {
if (brightness >= 0.0 && brightness <= 1.0) {
const screenBrightness = Math.round(brightness * 100);
setScreenBrightnessValue(screenBrightness);
}
});
}, [appService, settings, setSettings, getScreenBrightness]);
const debouncedSetScreenBrightness = useMemo(
() =>
debounce(async (value: number) => {
settings.screenBrightness = value;
setSettings(settings);
saveSettings(envConfig, settings);
await setScreenBrightness(value / 100);
}, 100),
[envConfig, settings, setSettings, saveSettings, setScreenBrightness],
);
const handleScreenBrightnessChange = useCallback(
async (value: number) => {
if (!appService?.isMobileApp) return;
setScreenBrightnessValue(value);
debouncedSetScreenBrightness(value);
},
[appService, debouncedSetScreenBrightness],
);
const cycleThemeMode = () => {
const nextMode = themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto';
setThemeMode(nextMode);
};
const classes = clsx(
'footerbar-color-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
actionTab === 'color'
? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out'
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
);
return (
<div className={classes} style={{ bottom: bottomOffset }}>
{appService?.hasScreenBrightness && (
<Slider
label={_('Screen Brightness')}
initialValue={screenBrightnessValue}
bubbleLabel={`${screenBrightnessValue}`}
minIcon={<PiSun size={16} />}
maxIcon={<PiSun size={24} />}
onChange={handleScreenBrightnessChange}
min={SCREEN_BRIGHTNESS_LIMITS.MIN}
max={SCREEN_BRIGHTNESS_LIMITS.MAX}
/>
)}
<div className='w-full'>
<div className='flex items-center justify-between p-2'>
<span className='text-sm font-medium'>{_('Color')}</span>
</div>
<div
className='flex gap-3 overflow-x-auto p-2'
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<style>{`
.theme-scroll::-webkit-scrollbar {
display: none;
}
`}</style>
{themes.map(({ name, label, colors }) => (
<button
key={name}
onClick={() => setThemeColor(name)}
className={clsx(
'flex flex-shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'h-[40px] min-w-[80px]',
themeColor === name
? 'ring-primary ring-offset-base-200 ring-2 ring-offset-2'
: 'hover:opacity-80',
)}
style={{
backgroundColor: isDarkMode ? colors.dark['base-100'] : colors.light['base-100'],
color: isDarkMode ? colors.dark['base-content'] : colors.light['base-content'],
}}
>
<span className='text-xs font-medium'>{_(label)}</span>
</button>
))}
<button
onClick={() => cycleThemeMode()}
className={clsx(
'flex flex-shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'h-[40px] min-w-[80px]',
themeMode === 'dark'
? 'ring-primary ring-offset-base-200 ring-2 ring-offset-2'
: 'hover:opacity-80',
)}
style={{
backgroundColor: (themes.find((t) => t.name === themeColor) || themes[0]!).colors
.dark['base-100'],
color: (themes.find((t) => t.name === themeColor) || themes[0]!).colors.dark[
'base-content'
],
}}
>
{themeMode === 'light' ? (
<PiSun size={20} />
) : themeMode === 'dark' ? (
<PiMoon size={20} />
) : (
<TbSunMoon size={20} />
)}
</button>
</div>
</div>
</div>
);
};
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useEffect } from 'react';
import { FaHeadphones } from 'react-icons/fa6';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
@@ -22,6 +22,25 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
const view = getView(bookKey);
const viewState = getViewState(bookKey);
const viewSettings = getViewSettings(bookKey);
const [progressValue, setProgressValue] = React.useState(
progressValid ? progressFraction * 100 : 0,
);
useEffect(() => {
if (progressValid) {
setProgressValue(progressFraction * 100);
}
}, [progressValid, progressFraction]);
const handleProgressChange = useCallback(
(value: number) => {
setProgressValue(value);
navigationHandlers.onProgressChange(value);
},
[navigationHandlers],
);
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
return (
@@ -78,8 +97,8 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
min={0}
max={100}
aria-label={_('Jump to Location')}
value={progressValid ? progressFraction * 100 : 0}
onChange={(e) => navigationHandlers.onProgressChange(parseInt(e.target.value, 10))}
value={progressValue}
onChange={(e) => handleProgressChange(parseInt(e.target.value, 10))}
/>
<Button
icon={<FaHeadphones className={viewState?.ttsEnabled ? 'text-blue-500' : ''} />}
@@ -30,14 +30,14 @@ const MARGIN_CONSTANTS = {
interface FontLayoutPanelProps {
bookKey: string;
actionTab: string;
mobileBottomOffset: string;
bottomOffset: string;
marginIconSize: number;
}
export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
bookKey,
actionTab,
mobileBottomOffset,
bottomOffset,
marginIconSize,
}) => {
const _ = useTranslation();
@@ -98,7 +98,7 @@ export const FontLayoutPanel: React.FC<FontLayoutPanelProps> = ({
);
return (
<div className={classes} style={{ bottom: mobileBottomOffset }}>
<div className={classes} style={{ bottom: bottomOffset }}>
<Slider
label={_('Font Size')}
initialValue={viewSettings?.defaultFontSize ?? FONT_SIZE_LIMITS.DEFAULT}
@@ -7,6 +7,7 @@ import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
import { FooterBarProps, NavigationHandlers, FooterBarChildProps } from './types';
import { debounce } from '@/utils/debounce';
import { viewPagination } from '../../hooks/usePagination';
import MobileFooterBar from './MobileFooterBar';
import DesktopFooterBar from './DesktopFooterBar';
@@ -50,10 +51,11 @@ const FooterBar: React.FC<FooterBarProps> = ({
return (progressInfo.current + 1) / progressInfo.total;
}, [progressValid, progressInfo]);
const handleProgressChange = useCallback(
(value: number) => {
view?.goToFraction(value / 100.0);
},
const handleProgressChange = useMemo(
() =>
debounce((value: number) => {
view?.goToFraction(value / 100.0);
}, 100),
[view],
);
@@ -1,9 +1,10 @@
import React from 'react';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { FooterBarChildProps } from './types';
import { NavigationPanel } from './NavigationPanel';
import { FontLayoutPanel } from './FontLayoutPanel';
import { ColorPanel } from './ColorPanel';
import { NavigationBar } from './NavigationBar';
import { FooterBarChildProps } from './types';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
const MobileFooterBar: React.FC<FooterBarChildProps> = ({
bookKey,
@@ -22,8 +23,9 @@ const MobileFooterBar: React.FC<FooterBarChildProps> = ({
return (
<>
<ColorPanel actionTab={actionTab} bottomOffset={bottomOffset} />
<NavigationPanel
actionTab={actionTab!}
actionTab={actionTab}
progressFraction={progressFraction}
progressValid={progressValid}
navigationHandlers={navigationHandlers}
@@ -32,13 +34,13 @@ const MobileFooterBar: React.FC<FooterBarChildProps> = ({
/>
<FontLayoutPanel
bookKey={bookKey}
actionTab={actionTab!}
mobileBottomOffset={bottomOffset}
actionTab={actionTab}
bottomOffset={bottomOffset}
marginIconSize={marginIconSize}
/>
<NavigationBar
bookKey={bookKey}
actionTab={actionTab!}
actionTab={actionTab}
navPadding={navPadding}
onSetActionTab={onSetActionTab!}
/>
@@ -1,9 +1,9 @@
import clsx from 'clsx';
import React from 'react';
import { IoIosList as TOCIcon } from 'react-icons/io';
import { PiNotePencil as NoteIcon } from 'react-icons/pi';
import { RxSlider as SliderIcon } from 'react-icons/rx';
import { RiFontFamily as FontIcon } from 'react-icons/ri';
import { PiSun as ColorIcon } from 'react-icons/pi';
import { MdOutlineHeadphones as TTSIcon } from 'react-icons/md';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -39,7 +39,11 @@ export const NavigationBar: React.FC<NavigationBarProps> = ({
icon={<TOCIcon size={tocIconSize} />}
onClick={() => onSetActionTab('toc')}
/>
<Button label={_('Notes')} icon={<NoteIcon />} onClick={() => onSetActionTab('note')} />
<Button
label={_('Color')}
icon={<ColorIcon className={clsx(actionTab === 'color' && 'text-blue-500')} />}
onClick={() => onSetActionTab('color')}
/>
<Button
label={_('Reading Progress')}
icon={<SliderIcon className={clsx(actionTab === 'progress' && 'text-blue-500')} />}
@@ -1,5 +1,5 @@
import React from 'react';
import clsx from 'clsx';
import React, { useCallback, useEffect } from 'react';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
@@ -30,6 +30,25 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
sliderHeight,
}) => {
const _ = useTranslation();
const [progressValue, setProgressValue] = React.useState(
progressValid ? progressFraction * 100 : 0,
);
useEffect(() => {
if (progressValid) {
setProgressValue(progressFraction * 100);
}
}, [progressValid, progressFraction]);
const handleProgressChange = useCallback(
(value: number) => {
setProgressValue(value);
navigationHandlers.onProgressChange(value);
},
[navigationHandlers],
);
const classes = clsx(
'footerbar-progress-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
actionTab === 'progress'
@@ -43,9 +62,9 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
<Slider
label={_('Reading Progress')}
heightPx={sliderHeight}
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
initialValue={progressValid ? progressFraction * 100 : 0}
onChange={navigationHandlers.onProgressChange}
bubbleLabel={`${Math.round(progressValue)}%`}
initialValue={progressValue}
onChange={handleProgressChange}
/>
</div>
<div className='flex w-full items-center justify-between gap-x-6'>
@@ -9,6 +9,7 @@ import { CSPostHogProvider } from '@/context/PHContext';
import { SyncProvider } from '@/context/SyncContext';
import { initSystemThemeListener, loadDataTheme } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useDeviceControlStore } from '@/store/deviceStore';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { getLocale } from '@/utils/misc';
@@ -16,6 +17,7 @@ import { getLocale } from '@/utils/misc';
const Providers = ({ children }: { children: React.ReactNode }) => {
const { appService } = useEnv();
const { applyUILanguage } = useSettingsStore();
const { setScreenBrightness } = useDeviceControlStore();
const iconSize = useDefaultIconSize();
useSafeAreaInsets(); // Initialize safe area insets
@@ -38,9 +40,13 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
initSystemThemeListener(appService);
appService.loadSettings().then((settings) => {
applyUILanguage(settings.globalViewSettings?.uiLanguage);
const brightness = settings.screenBrightness;
if (appService.hasScreenBrightness && brightness >= 0) {
setScreenBrightness(brightness / 100);
}
});
}
}, [appService, applyUILanguage]);
}, [appService, applyUILanguage, setScreenBrightness]);
// Make sure appService is available in all children components
if (!appService) return;
+10 -5
View File
@@ -9,6 +9,8 @@ interface SliderProps {
heightPx?: number;
minLabel?: string;
maxLabel?: string;
minIcon?: React.ReactNode;
maxIcon?: React.ReactNode;
bubbleElement?: React.ReactNode;
bubbleLabel?: string;
className?: string;
@@ -27,6 +29,8 @@ const Slider: React.FC<SliderProps> = ({
heightPx = 40,
minLabel = '',
maxLabel = '',
minIcon,
maxIcon,
bubbleElement,
bubbleLabel = '',
className = '',
@@ -63,6 +67,7 @@ const Slider: React.FC<SliderProps> = ({
}, [initialValue]);
const percentage = ((value - min) / (max - min)) * 100;
const visualPercentage = (percentage / 100) * 95;
return (
<div
@@ -79,22 +84,22 @@ const Slider: React.FC<SliderProps> = ({
className='bg-base-300 absolute h-full rounded-full'
style={{
width:
percentage > 0
? `max(calc(${percentage}% + ${heightPx / 2}px), ${heightPx}px)`
visualPercentage > 0
? `max(calc(${visualPercentage}% + ${heightPx / 2}px), ${heightPx}px)`
: '0px',
[isRtl ? 'right' : 'left']: 0,
}}
></div>
{/* Min/Max labels */}
<div className='absolute inset-0 flex items-center justify-between px-4 text-sm'>
<span className={`ml-2 ${minClassName}`}>{minLabel}</span>
<span className={`mr-2 ${maxClassName}`}>{maxLabel}</span>
{minIcon ? minIcon : <span className={`ml-2 ${minClassName}`}>{minLabel}</span>}
{maxIcon ? maxIcon : <span className={`mr-2 ${maxClassName}`}>{maxLabel}</span>}
</div>
{/* Thumb bubble */}
<div
className='pointer-events-none absolute top-0 z-10'
style={{
[isRtl ? 'right' : 'left']: `max(${heightPx / 2}px, calc(${percentage}%))`,
[isRtl ? 'right' : 'left']: `max(${heightPx / 2}px, calc(${visualPercentage}%))`,
transform: isRtl ? 'translateX(calc(50%))' : 'translateX(calc(-50%))',
height: '100%',
}}
@@ -86,6 +86,7 @@ export abstract class BaseAppService implements AppService {
hasHaptics = false;
hasUpdater = false;
hasOrientationLock = false;
hasScreenBrightness = false;
canCustomizeRootDir = false;
distChannel = 'readest' as DistChannel;
@@ -60,6 +60,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
alwaysInForeground: false,
autoCheckUpdates: true,
screenWakeLock: false,
screenBrightness: -1, // -1~100, -1 for system default
openLastBooks: false,
lastOpenBooks: [],
autoImportBooksOnOpen: false,
@@ -348,6 +348,7 @@ export class NativeAppService extends BaseAppService {
// orientation lock is not supported on iPad
override hasOrientationLock =
(OS_TYPE === 'ios' && getOSPlatform() === 'ios') || OS_TYPE === 'android';
override hasScreenBrightness = OS_TYPE === 'ios' || OS_TYPE === 'android';
// CustomizeRootDir has a blocker on macOS App Store builds due to Security Scoped Resource restrictions.
// See: https://github.com/tauri-apps/tauri/issues/3716
override canCustomizeRootDir = process.env['NEXT_PUBLIC_DIST_CHANNEL'] !== 'appstore';
+12 -1
View File
@@ -1,5 +1,5 @@
import { create } from 'zustand';
import { interceptKeys } from '@/utils/bridge';
import { interceptKeys, getScreenBrightness, setScreenBrightness } from '@/utils/bridge';
import { eventDispatcher } from '@/utils/event';
declare global {
@@ -23,6 +23,8 @@ type DeviceControlState = {
backKeyIntercepted: boolean;
volumeKeysInterceptionCount: number;
backKeyInterceptionCount: number;
getScreenBrightness: () => Promise<number>; // 0.0 to 1.0
setScreenBrightness: (brightness: number) => Promise<void>; // brightness: 0.0 to 1.0
acquireVolumeKeyInterception: () => void;
releaseVolumeKeyInterception: () => void;
acquireBackKeyInterception: () => void;
@@ -74,4 +76,13 @@ export const useDeviceControlStore = create<DeviceControlState>((set, get) => ({
set({ backKeyInterceptionCount: backKeyInterceptionCount - 1 });
}
},
getScreenBrightness: async () => {
const res = await getScreenBrightness();
return res.brightness;
},
setScreenBrightness: async (brightness: number) => {
await setScreenBrightness({ brightness });
},
}));
+1
View File
@@ -46,6 +46,7 @@ export interface SystemSettings {
openBookInNewWindow: boolean;
autoCheckUpdates: boolean;
screenWakeLock: boolean;
screenBrightness: number;
alwaysShowStatusBar: boolean;
alwaysInForeground: boolean;
openLastBooks: boolean;
+1
View File
@@ -51,6 +51,7 @@ export interface AppService {
hasHaptics: boolean;
hasUpdater: boolean;
hasOrientationLock: boolean;
hasScreenBrightness: boolean;
isMobile: boolean;
isAppDataSandbox: boolean;
isMobileApp: boolean;
+33
View File
@@ -65,6 +65,20 @@ export interface GetSafeAreaInsetsResponse {
error?: string;
}
interface GetScreenBrightnessResponse {
brightness: number; // 0.0 to 1.0
error?: string;
}
interface SetScreenBrightnessRequest {
brightness: number; // 0.0 to 1.0
}
interface SetScreenBrightnessResponse {
success: boolean;
error?: string;
}
export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIResponse> {
const result = await invoke<CopyURIResponse>('plugin:native-bridge|copy_uri_to_path', {
payload: request,
@@ -145,3 +159,22 @@ export async function getSafeAreaInsets(): Promise<GetSafeAreaInsetsResponse> {
);
return result;
}
export async function getScreenBrightness(): Promise<GetScreenBrightnessResponse> {
const result = await invoke<GetScreenBrightnessResponse>(
'plugin:native-bridge|get_screen_brightness',
);
return result;
}
export async function setScreenBrightness(
request: SetScreenBrightnessRequest,
): Promise<SetScreenBrightnessResponse> {
const result = await invoke<SetScreenBrightnessResponse>(
'plugin:native-bridge|set_screen_brightness',
{
payload: request,
},
);
return result;
}