forked from akai/readest
feat: retrieve system fonts on iOS and Android and show font weight variants, closes #949 and closes #557 (#976)
This commit is contained in:
Generated
+1
-1
@@ -14,7 +14,6 @@ version = "0.2.2"
|
||||
dependencies = [
|
||||
"block",
|
||||
"cocoa",
|
||||
"font-enumeration",
|
||||
"futures-util",
|
||||
"log",
|
||||
"objc",
|
||||
@@ -5438,6 +5437,7 @@ dependencies = [
|
||||
name = "tauri-plugin-native-bridge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"font-enumeration",
|
||||
"schemars",
|
||||
"serde",
|
||||
"tauri",
|
||||
|
||||
@@ -63,4 +63,3 @@ tauri-plugin-cli = "2"
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-updater = "2.7.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
font-enumeration = "0.9.0"
|
||||
|
||||
@@ -17,3 +17,6 @@ schemars = "0.8"
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.4", features = ["build"] }
|
||||
schemars = "0.8"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
font-enumeration = "0.9.0"
|
||||
|
||||
+42
@@ -11,6 +11,8 @@ import android.view.WindowManager
|
||||
import android.view.WindowInsetsController
|
||||
import android.graphics.Color
|
||||
import android.webkit.WebView
|
||||
import android.graphics.fonts.SystemFonts
|
||||
import android.graphics.fonts.Font
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@@ -22,6 +24,7 @@ import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import org.json.JSONArray
|
||||
import java.io.*
|
||||
|
||||
@InvokeArg
|
||||
@@ -247,4 +250,43 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_sys_fonts_list(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val fontList = mutableListOf<String>()
|
||||
val fontFileList = mutableListOf<String>()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val systemFonts = SystemFonts.getAvailableFonts()
|
||||
for (font in systemFonts) {
|
||||
val file = font.getFile()?: continue
|
||||
if (file.isFile && (file.name.endsWith(".ttf", true) || file.name.endsWith(".otf", true))) {
|
||||
fontFileList.add(file.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val fontDirs = listOf("/system/fonts", "/system/font", "/data/fonts")
|
||||
for (dirPath in fontDirs) {
|
||||
val dir = File(dirPath)
|
||||
if (dir.exists() && dir.isDirectory) {
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isFile && (file.name.endsWith(".ttf", true) || file.name.endsWith(".otf", true))) {
|
||||
fontFileList.add(file.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (fileFileName in fontFileList) {
|
||||
var fontName = fileFileName
|
||||
.replace(Regex("\\.(ttf|otf)$", RegexOption.IGNORE_CASE), "")
|
||||
.trim()
|
||||
}
|
||||
ret.put("fonts", JSONArray(fontList))
|
||||
} catch (e: Exception) {
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const COMMANDS: &[&str] = &[
|
||||
"install_package",
|
||||
"set_system_ui_visibility",
|
||||
"get_status_bar_height",
|
||||
"get_sys_fonts_list",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
+36
-1
@@ -1,11 +1,27 @@
|
||||
import AuthenticationServices
|
||||
import AVFoundation
|
||||
import AuthenticationServices
|
||||
import CoreText
|
||||
import MediaPlayer
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
func getLocalizedDisplayName(familyName: String) -> String? {
|
||||
let fontDescriptor = CTFontDescriptorCreateWithAttributes(
|
||||
[
|
||||
kCTFontFamilyNameAttribute: familyName
|
||||
] as CFDictionary)
|
||||
|
||||
let font = CTFontCreateWithFontDescriptor(fontDescriptor, 0.0, nil)
|
||||
|
||||
var actualLanguage: Unmanaged<CFString>?
|
||||
if let localizedName = CTFontCopyLocalizedName(font, kCTFontFamilyNameKey, &actualLanguage) {
|
||||
return localizedName as String
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
class SafariAuthRequestArgs: Decodable {
|
||||
let authUrl: String
|
||||
}
|
||||
@@ -95,6 +111,25 @@ class NativeBridgePlugin: Plugin {
|
||||
}
|
||||
invoke.resolve(["success": true])
|
||||
}
|
||||
|
||||
@objc public func get_sys_fonts_list(_ invoke: Invoke) throws {
|
||||
var fontList: [String] = []
|
||||
|
||||
for family in UIFont.familyNames.sorted() {
|
||||
if let localized = getLocalizedDisplayName(familyName: family) {
|
||||
fontList.append(localized)
|
||||
} else {
|
||||
let fontNames = UIFont.fontNames(forFamilyName: family)
|
||||
if fontNames.isEmpty {
|
||||
fontList.append(family)
|
||||
} else {
|
||||
fontList.append(contentsOf: fontNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invoke.resolve(["fonts": fontList])
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_native_bridge")
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-sys-fonts-list"
|
||||
description = "Enables the get_sys_fonts_list command without any pre-configured scope."
|
||||
commands.allow = ["get_sys_fonts_list"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-sys-fonts-list"
|
||||
description = "Denies the get_sys_fonts_list command without any pre-configured scope."
|
||||
commands.deny = ["get_sys_fonts_list"]
|
||||
+27
@@ -11,6 +11,7 @@ Default permissions for the plugin
|
||||
- `allow-install-package`
|
||||
- `allow-set-system-ui-visibility`
|
||||
- `allow-get-status-bar-height`
|
||||
- `allow-get-sys-fonts-list`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -128,6 +129,32 @@ Denies the get_status_bar_height command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-sys-fonts-list`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_sys_fonts_list command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-get-sys-fonts-list`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_sys_fonts_list command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-install-package`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -8,4 +8,5 @@ permissions = [
|
||||
"allow-install-package",
|
||||
"allow-set-system-ui-visibility",
|
||||
"allow-get-status-bar-height",
|
||||
"allow-get-sys-fonts-list",
|
||||
]
|
||||
|
||||
+14
-2
@@ -342,6 +342,18 @@
|
||||
"const": "deny-get-status-bar-height",
|
||||
"markdownDescription": "Denies the get_status_bar_height command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_sys_fonts_list command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-sys-fonts-list",
|
||||
"markdownDescription": "Enables the get_sys_fonts_list command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_sys_fonts_list command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-sys-fonts-list",
|
||||
"markdownDescription": "Denies the get_sys_fonts_list command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the install_package command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -379,10 +391,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`",
|
||||
"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`",
|
||||
"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`"
|
||||
"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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -58,3 +58,10 @@ pub(crate) async fn get_status_bar_height<R: Runtime>(
|
||||
) -> Result<GetStatusBarHeightResponse> {
|
||||
app.native_bridge().get_status_bar_height()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_sys_fonts_list<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<GetSysFontsListResponse> {
|
||||
app.native_bridge().get_sys_fonts_list()
|
||||
}
|
||||
|
||||
@@ -47,4 +47,13 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_status_bar_height(&self) -> crate::Result<GetStatusBarHeightResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn get_sys_fonts_list(&self) -> crate::Result<GetSysFontsListResponse> {
|
||||
let font_collection = font_enumeration::Collection::new().unwrap();
|
||||
let mut fonts = Vec::new();
|
||||
for font in font_collection.all() {
|
||||
fonts.push(font.font_name.clone());
|
||||
}
|
||||
Ok(GetSysFontsListResponse { fonts, error: None })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::install_package,
|
||||
commands::set_system_ui_visibility,
|
||||
commands::get_status_bar_height,
|
||||
commands::get_sys_fonts_list,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -85,3 +85,11 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_sys_fonts_list(&self) -> crate::Result<GetSysFontsListResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_sys_fonts_list", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,10 @@ pub struct GetStatusBarHeightResponse {
|
||||
pub height: u32,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSysFontsListResponse {
|
||||
pub fonts: Vec<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -105,17 +105,6 @@ async fn start_server(window: Window) -> Result<u16, String> {
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[command]
|
||||
async fn list_fonts() -> Result<Vec<String>, String> {
|
||||
let font_collection = font_enumeration::Collection::new().unwrap();
|
||||
let mut fonts = Vec::new();
|
||||
for font in font_collection.all() {
|
||||
fonts.push(font.family_name.clone());
|
||||
}
|
||||
Ok(fonts)
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Payload {
|
||||
@@ -138,8 +127,6 @@ pub fn run() {
|
||||
macos::apple_auth::start_apple_sign_in,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::traffic_light::set_traffic_lights,
|
||||
#[cfg(desktop)]
|
||||
list_fonts
|
||||
])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { getStoragePlanData } from '@/utils/access';
|
||||
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
|
||||
@@ -37,6 +38,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const [isAutoImportBooksOnOpen, setIsAutoImportBooksOnOpen] = useState(
|
||||
settings.autoImportBooksOnOpen,
|
||||
);
|
||||
const iconSize = useResponsiveSize(16);
|
||||
|
||||
const showAboutReadest = () => {
|
||||
setAboutDialogVisible(true);
|
||||
@@ -147,10 +149,10 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={_('User avatar')}
|
||||
className='h-5 w-5 rounded-full'
|
||||
className='rounded-full'
|
||||
referrerPolicy='no-referrer'
|
||||
width={20}
|
||||
height={20}
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
/>
|
||||
) : (
|
||||
PiUserCircleCheck
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { FiChevronUp, FiChevronLeft } from 'react-icons/fi';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface DropdownProps {
|
||||
family?: string;
|
||||
@@ -24,33 +24,40 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const defaultIconSize = useDefaultIconSize();
|
||||
const allOptions = [...options, ...(moreOptions ?? [])];
|
||||
const selectedOption = allOptions.find((option) => option.option === selected) ?? allOptions[0]!;
|
||||
return (
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
|
||||
className='btn btn-sm flex items-center px-[10px] font-normal normal-case sm:px-[20px]'
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
>
|
||||
<span style={{ fontFamily: onGetFontFamily(selectedOption.option, family ?? '') }}>
|
||||
{selectedOption.label}
|
||||
</span>
|
||||
<FiChevronUp size={iconSize16} />
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span
|
||||
className='text-ellipsis'
|
||||
style={{
|
||||
fontFamily: onGetFontFamily(selectedOption.option, family ?? ''),
|
||||
}}
|
||||
>
|
||||
{selectedOption.label}
|
||||
</span>
|
||||
<FiChevronUp size={iconSize16} />
|
||||
</div>
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute right-[-32px] z-[1] mt-4 w-44 shadow sm:right-0',
|
||||
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute z-[1] mt-4 shadow',
|
||||
'!sm:px-2 right-[-32px] w-[46vw] !px-1 sm:right-0 sm:w-44',
|
||||
moreOptions?.length ? '' : 'inline max-h-80 overflow-y-scroll',
|
||||
)}
|
||||
>
|
||||
{options.map(({ option, label }) => (
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
<div className='flex items-center px-0'>
|
||||
<span style={{ minWidth: `${defaultIconSize}px` }}>
|
||||
{selected === option && <MdCheck className='text-base-content' />}
|
||||
<div className='flex w-full items-center overflow-hidden px-0 text-sm'>
|
||||
<span style={{ minWidth: `${iconSize16}px` }}>
|
||||
{selected === option && <MdCheck className='text-base-content' size={iconSize16} />}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
|
||||
{label || option}
|
||||
@@ -60,8 +67,8 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
))}
|
||||
{moreOptions && moreOptions.length > 0 && (
|
||||
<li className='dropdown dropdown-left dropdown-top'>
|
||||
<div className='flex items-center px-0'>
|
||||
<span style={{ minWidth: `${defaultIconSize}px` }}>
|
||||
<div className='flex items-center px-0 text-sm'>
|
||||
<span style={{ minWidth: `${iconSize16}px` }}>
|
||||
<FiChevronLeft size={iconSize16} />
|
||||
</span>
|
||||
<span>{_('System Fonts')}</span>
|
||||
@@ -69,15 +76,17 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'dropdown-content bgcolor-base-200 menu rounded-box relative z-[1] overflow-y-scroll shadow',
|
||||
'!mr-5 mb-[-46px] inline max-h-80 w-[200px] overflow-y-scroll',
|
||||
'dropdown-content bgcolor-base-200 menu rounded-box relative z-[1] shadow',
|
||||
'!sm:px-2 !mr-4 mb-[-46px] inline max-h-80 w-[46vw] overflow-y-scroll !px-1 sm:w-[200px]',
|
||||
)}
|
||||
>
|
||||
{moreOptions.map((option, index) => (
|
||||
<li key={`${index}-${option.option}`} onClick={() => onSelect(option.option)}>
|
||||
<div className='flex items-center px-2'>
|
||||
<span style={{ minWidth: `${defaultIconSize}px` }}>
|
||||
{selected === option.option && <MdCheck className='text-base-content' />}
|
||||
<div className='flex w-full items-center overflow-hidden px-0 text-sm'>
|
||||
<span style={{ minWidth: `${iconSize16}px` }}>
|
||||
{selected === option.option && (
|
||||
<MdCheck className='text-base-content' size={iconSize16} />
|
||||
)}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option.option, family ?? '') }}>
|
||||
{option.label || option.option}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
ANDROID_FONTS,
|
||||
CJK_EXCLUDE_PATTENS,
|
||||
CJK_FONTS_PATTENS,
|
||||
CJK_NAMES_PATTENS,
|
||||
CJK_SANS_SERIF_FONTS,
|
||||
@@ -19,12 +20,21 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { getOSPlatform, isCJKEnv } from '@/utils/misc';
|
||||
import { getSysFontsList } from '@/utils/font';
|
||||
import { getSysFontsList } from '@/utils/bridge';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { saveViewSettings } from '../../utils/viewSettingsHelper';
|
||||
import NumberInput from './NumberInput';
|
||||
import FontDropdown from './FontDropDown';
|
||||
|
||||
const genCJKFontsList = (sysFonts: string[]) => {
|
||||
return Array.from(new Set([...sysFonts, ...CJK_SERIF_FONTS, ...CJK_SANS_SERIF_FONTS]))
|
||||
.filter((font) => CJK_FONTS_PATTENS.test(font) || CJK_NAMES_PATTENS.test(font))
|
||||
.filter((font) => !CJK_EXCLUDE_PATTENS.test(font))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
};
|
||||
|
||||
const isSymbolicFontName = (font: string) =>
|
||||
/emoji|icons|symbol|dingbats|ornaments|webdings|wingdings|miuiex/i.test(font);
|
||||
interface FontFaceProps {
|
||||
className?: string;
|
||||
family: string;
|
||||
@@ -51,7 +61,7 @@ const FontFace = ({
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className=''>{label}</span>
|
||||
<span className='min-w-10'>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options.map((option) => ({ option, label: _(option) }))}
|
||||
@@ -66,7 +76,7 @@ const FontFace = ({
|
||||
|
||||
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { envConfig } = useEnv();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const view = getView(bookKey)!;
|
||||
@@ -113,19 +123,28 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
|
||||
const [monospaceFont, setMonospaceFont] = useState(viewSettings.monospaceFont!);
|
||||
const [fontWeight, setFontWeight] = useState(viewSettings.fontWeight!);
|
||||
const [CJKFonts] = useState<string[]>(() => {
|
||||
return Array.from(new Set([...sysFonts, ...CJK_SERIF_FONTS, ...CJK_SANS_SERIF_FONTS]))
|
||||
.filter((font) => CJK_FONTS_PATTENS.test(font) || CJK_NAMES_PATTENS.test(font))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const [CJKFonts, setCJKFonts] = useState<string[]>(() => {
|
||||
return genCJKFontsList(sysFonts);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isTauriAppPlatform() && appService?.hasSysFontsList) {
|
||||
getSysFontsList().then((fonts) => {
|
||||
setSysFonts(fonts);
|
||||
setCJKFonts((prev) => {
|
||||
const newFonts = genCJKFontsList(sysFonts);
|
||||
return prev.length !== newFonts.length ? newFonts : prev;
|
||||
});
|
||||
}, [sysFonts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTauriAppPlatform()) {
|
||||
getSysFontsList().then((res) => {
|
||||
if (res.error) {
|
||||
console.error('Failed to get system fonts list:', res.error);
|
||||
return;
|
||||
}
|
||||
const fonts = res.fonts.filter((font) => font && !isSymbolicFontName(font));
|
||||
setSysFonts([...new Set(fonts)].sort((a, b) => a.localeCompare(b)));
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -58,7 +58,6 @@ export abstract class BaseAppService implements AppService {
|
||||
hasRoundedWindow = false;
|
||||
hasSafeAreaInset = false;
|
||||
hasHaptics = false;
|
||||
hasSysFontsList = false;
|
||||
hasUpdater = false;
|
||||
|
||||
abstract fs: FileSystem;
|
||||
|
||||
@@ -434,6 +434,10 @@ export const ANDROID_FONTS = [
|
||||
];
|
||||
|
||||
export const CJK_NAMES_PATTENS = /[\u3040-\u30FF\u4E00-\u9FFF\uAC00-\uD7AF]/;
|
||||
export const CJK_EXCLUDE_PATTENS = new RegExp(
|
||||
['AlBayan', 'STIX', 'Kailasa', 'ITCTT', 'Luminari', 'Myanmar'].join('|'),
|
||||
'i',
|
||||
);
|
||||
export const CJK_FONTS_PATTENS = new RegExp(
|
||||
[
|
||||
'CJK',
|
||||
@@ -461,7 +465,6 @@ export const CJK_FONTS_PATTENS = new RegExp(
|
||||
'Yu\\s?Gothic',
|
||||
'Yu\\s?Mincho',
|
||||
'Mincho',
|
||||
'Gothic',
|
||||
'Nanum',
|
||||
'Malgun',
|
||||
'Gulim',
|
||||
|
||||
@@ -220,7 +220,6 @@ export class NativeAppService extends BaseAppService {
|
||||
override hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android') && !!window.IS_ROUNDED;
|
||||
override hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
override hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
override hasSysFontsList = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
override hasUpdater = OS_TYPE !== 'ios' && !process.env['NEXT_PUBLIC_DISABLE_UPDATER'];
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
|
||||
@@ -33,7 +33,6 @@ export interface AppService {
|
||||
hasRoundedWindow: boolean;
|
||||
hasSafeAreaInset: boolean;
|
||||
hasHaptics: boolean;
|
||||
hasSysFontsList: boolean;
|
||||
hasUpdater: boolean;
|
||||
isMobile: boolean;
|
||||
isAppDataSandbox: boolean;
|
||||
|
||||
@@ -38,6 +38,11 @@ export interface GetStatusBarHeightResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface GetSystemFontsListResponse {
|
||||
fonts: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIResponse> {
|
||||
const result = await invoke<CopyURIResponse>('plugin:native-bridge|copy_uri_to_path', {
|
||||
payload: request,
|
||||
@@ -79,3 +84,16 @@ export async function getStatusBarHeight(): Promise<GetStatusBarHeightResponse>
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
let cachedSysFontsResult: GetSystemFontsListResponse | null = null;
|
||||
|
||||
export async function getSysFontsList(): Promise<GetSystemFontsListResponse> {
|
||||
if (cachedSysFontsResult) {
|
||||
return cachedSysFontsResult;
|
||||
}
|
||||
const result = await invoke<GetSystemFontsListResponse>(
|
||||
'plugin:native-bridge|get_sys_fonts_list',
|
||||
);
|
||||
cachedSysFontsResult = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getOSPlatform } from './misc';
|
||||
|
||||
let cachedSysFonts: string[] | null = null;
|
||||
|
||||
export const FONT_ENUM_SUPPORTED_OS_PLATFORMS = ['macos', 'windows', 'linux'];
|
||||
|
||||
const isSymbolicFontName = (font: string) =>
|
||||
/emoji|icons|symbol|dingbats|ornaments|webdings|wingdings/i.test(font);
|
||||
|
||||
export const getSysFontsList = async (): Promise<string[]> => {
|
||||
if (cachedSysFonts) {
|
||||
return cachedSysFonts;
|
||||
}
|
||||
|
||||
try {
|
||||
const osPlatform = getOSPlatform();
|
||||
if (FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
|
||||
const fonts = await invoke<string[]>('list_fonts');
|
||||
cachedSysFonts = [...new Set(fonts.filter((font) => !isSymbolicFontName(font)))].sort();
|
||||
return cachedSysFonts;
|
||||
} else {
|
||||
console.warn(`Unsupported platform: ${osPlatform}`);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching font list:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user