From 48920a87bfa1b1cc6c8ecf65d05ef3a10b82a040 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Thu, 22 Jan 2026 11:42:16 +0100 Subject: [PATCH] chore(opds): disable popular online opds catalogs in certain regions on App Store (#3031) --- .../tauri-plugin-native-bridge/build.rs | 1 + .../ios/Sources/NativeBridgePlugin.swift | 10 +++ .../commands/get_storefront_region_code.toml | 13 +++ .../permissions/autogenerated/reference.md | 27 +++++++ .../permissions/default.toml | 1 + .../permissions/schemas/schema.json | 16 +++- .../src/commands.rs | 7 ++ .../tauri-plugin-native-bridge/src/desktop.rs | 4 + .../tauri-plugin-native-bridge/src/lib.rs | 1 + .../tauri-plugin-native-bridge/src/mobile.rs | 8 ++ .../tauri-plugin-native-bridge/src/models.rs | 7 ++ .../src/app/library/components/ImportMenu.tsx | 9 ++- .../src/app/library/components/OPDSDialog.tsx | 4 +- .../app/opds/components/CatelogManager.tsx | 80 ++++++++++--------- apps/readest-app/src/services/appService.ts | 2 + .../src/services/nativeAppService.ts | 12 ++- apps/readest-app/src/types/system.ts | 2 + apps/readest-app/src/utils/bridge.ts | 12 +++ 18 files changed, 170 insertions(+), 46 deletions(-) create mode 100644 apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/get_storefront_region_code.toml diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs index c61a92e9..ece34b20 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs @@ -21,6 +21,7 @@ const COMMANDS: &[&str] = &[ "get_external_sdcard_path", "open_external_url", "select_directory", + "get_storefront_region_code", "register_listener", "remove_listener", "request_manage_storage_permission", diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift index f573b4b5..9537a8ca 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift @@ -879,6 +879,16 @@ class NativeBridgePlugin: Plugin { invoke.reject("Failed to copy file: \(error.localizedDescription)") } } + + @objc public func get_storefront_region_code(_ invoke: Invoke) { + Task { + if let storefront = await Storefront.current { + invoke.resolve(["regionCode": storefront.countryCode]) + } else { + invoke.reject("Failed to get region code") + } + } + } } @_cdecl("init_plugin_native_bridge") diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/get_storefront_region_code.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/get_storefront_region_code.toml new file mode 100644 index 00000000..5f0c6fda --- /dev/null +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/get_storefront_region_code.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-storefront-region-code" +description = "Enables the get_storefront_region_code command without any pre-configured scope." +commands.allow = ["get_storefront_region_code"] + +[[permission]] +identifier = "deny-get-storefront-region-code" +description = "Denies the get_storefront_region_code command without any pre-configured scope." +commands.deny = ["get_storefront_region_code"] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md index 2a1b59e4..0b67e542 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md @@ -26,6 +26,7 @@ Default permissions for the plugin - `allow-get-external-sdcard-path` - `allow-open-external-url` - `allow-select-directory` +- `allow-get-storefront-region-code` - `allow-request-manage-storage-permission` - `allow-register-listener` - `allow-remove-listener` @@ -306,6 +307,32 @@ Denies the get_status_bar_height command without any pre-configured scope. +`native-bridge:allow-get-storefront-region-code` + + + + +Enables the get_storefront_region_code command without any pre-configured scope. + + + + + + + +`native-bridge:deny-get-storefront-region-code` + + + + +Denies the get_storefront_region_code command without any pre-configured scope. + + + + + + + `native-bridge:allow-get-sys-fonts-list` diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml index 83b54f92..e9fc5e8a 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml @@ -23,6 +23,7 @@ permissions = [ "allow-get-external-sdcard-path", "allow-open-external-url", "allow-select-directory", + "allow-get-storefront-region-code", "allow-request-manage-storage-permission", "allow-register-listener", "allow-remove-listener", diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json index a3684de1..cb0cc2e3 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json @@ -414,6 +414,18 @@ "const": "deny-get-status-bar-height", "markdownDescription": "Denies the get_status_bar_height command without any pre-configured scope." }, + { + "description": "Enables the get_storefront_region_code command without any pre-configured scope.", + "type": "string", + "const": "allow-get-storefront-region-code", + "markdownDescription": "Enables the get_storefront_region_code command without any pre-configured scope." + }, + { + "description": "Denies the get_storefront_region_code command without any pre-configured scope.", + "type": "string", + "const": "deny-get-storefront-region-code", + "markdownDescription": "Denies the get_storefront_region_code command without any pre-configured scope." + }, { "description": "Enables the get_sys_fonts_list command without any pre-configured scope.", "type": "string", @@ -667,10 +679,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-is-available`\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-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\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-is-available`\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-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-get-storefront-region-code`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\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-is-available`\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-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\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-is-available`\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-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-get-storefront-region-code`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`" } ] } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs index 7cc77679..a8b9c040 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs @@ -186,6 +186,13 @@ pub(crate) async fn select_directory( Ok(result) } +#[command] +pub(crate) async fn get_storefront_region_code( + app: AppHandle, +) -> Result { + app.native_bridge().get_storefront_region_code() +} + #[command] pub(crate) async fn request_manage_storage_permission( app: AppHandle, diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs index 2550e2e1..a312dd42 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs @@ -137,6 +137,10 @@ impl NativeBridge { Err(crate::Error::UnsupportedPlatformError) } + pub fn get_storefront_region_code(&self) -> crate::Result { + Err(crate::Error::UnsupportedPlatformError) + } + pub fn request_manage_storage_permission( &self, ) -> crate::Result { diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs index 91701051..c4d74818 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs @@ -77,6 +77,7 @@ pub fn init() -> TauriPlugin { commands::get_external_sdcard_path, commands::open_external_url, commands::select_directory, + commands::get_storefront_region_code, commands::request_manage_storage_permission, ]) .setup(|app, api| { diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs index 6f3e7b45..515e7885 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs @@ -224,6 +224,14 @@ impl NativeBridge { } } +impl NativeBridge { + pub fn get_storefront_region_code(&self) -> crate::Result { + self.0 + .run_mobile_plugin("get_storefront_region_code", ()) + .map_err(Into::into) + } +} + impl NativeBridge { pub fn request_manage_storage_permission( &self, diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs index c19ba668..3736a199 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs @@ -230,3 +230,10 @@ pub struct SelectDirectoryResponse { pub path: Option, pub error: Option, } + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetStorefrontRegionCodeResponse { + pub region_code: Option, + pub error: Option, +} diff --git a/apps/readest-app/src/app/library/components/ImportMenu.tsx b/apps/readest-app/src/app/library/components/ImportMenu.tsx index e4816fc1..a7dc7c01 100644 --- a/apps/readest-app/src/app/library/components/ImportMenu.tsx +++ b/apps/readest-app/src/app/library/components/ImportMenu.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx'; -import { useTranslation } from '@/hooks/useTranslation'; -import { IoFileTray } from 'react-icons/io5'; import { MdRssFeed } from 'react-icons/md'; - +import { IoFileTray } from 'react-icons/io5'; +import { useEnv } from '@/context/EnvContext'; +import { useTranslation } from '@/hooks/useTranslation'; import MenuItem from '@/components/MenuItem'; import Menu from '@/components/Menu'; @@ -20,6 +20,7 @@ const ImportMenu: React.FC = ({ onOpenCatalogManager, }) => { const _ = useTranslation(); + const { appService } = useEnv(); const handleImportFromFiles = () => { onImportBooksFromFiles(); @@ -54,7 +55,7 @@ const ImportMenu: React.FC = ({ /> )} } onClick={handleOpenCatalogManager} /> diff --git a/apps/readest-app/src/app/library/components/OPDSDialog.tsx b/apps/readest-app/src/app/library/components/OPDSDialog.tsx index 4e92a870..3cdb72cc 100644 --- a/apps/readest-app/src/app/library/components/OPDSDialog.tsx +++ b/apps/readest-app/src/app/library/components/OPDSDialog.tsx @@ -1,4 +1,5 @@ import { clsx } from 'clsx'; +import { useEnv } from '@/context/EnvContext'; import { CatalogManager } from '@/app/opds/components/CatelogManager'; import { useTranslation } from '@/hooks/useTranslation'; import Dialog from '@/components/Dialog'; @@ -9,10 +10,11 @@ interface CatalogDialogProps { export function CatalogDialog({ onClose }: CatalogDialogProps) { const _ = useTranslation(); + const { appService } = useEnv(); return ( (() => settings.opdsCatalogs || []); const [showAddDialog, setShowAddDialog] = useState(false); @@ -70,6 +71,7 @@ export function CatalogManager() { const [showPassword, setShowPassword] = useState(false); const [urlError, setUrlError] = useState(''); const [isValidating, setIsValidating] = useState(false); + const popularCatalogs = appService?.isOnlineCatalogsAccessible ? POPULAR_CATALOGS : []; const saveCatalogs = (updatedCatalogs: OPDSCatalog[]) => { setCatalogs(updatedCatalogs); @@ -233,48 +235,50 @@ export function CatalogManager() { {/* Popular Catalogs */} -
+

{_('Popular Catalogs')}

- {POPULAR_CATALOGS.filter((catalog) => !catalog.disabled).map((catalog) => { - const isAdded = catalogs.some((c) => c.url === catalog.url); - return ( -
-
-

- {catalog.icon && {catalog.icon}} - {catalog.name} -

- {catalog.description && ( -

- {catalog.description} -

- )} -
- {!isAdded && ( - + {popularCatalogs + .filter((catalog) => !catalog.disabled) + .map((catalog) => { + const isAdded = catalogs.some((c) => c.url === catalog.url); + return ( +
+
+

+ {catalog.icon && {catalog.icon}} + {catalog.name} +

+ {catalog.description && ( +

+ {catalog.description} +

)} - +
+ {!isAdded && ( + + )} + +
-
- ); - })} + ); + })}
diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index 746ec645..5bd7b608 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -111,6 +111,8 @@ export abstract class BaseAppService implements AppService { canCustomizeRootDir = false; canReadExternalDir = false; distChannel = 'readest' as DistChannel; + storefrontRegionCode: string | null = null; + isOnlineCatalogsAccessible = true; protected CURRENT_MIGRATION_VERSION = 20251124; diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 43f16743..74bac8cc 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -38,7 +38,7 @@ import { import { getOSPlatform, isContentURI, isFileURI, isValidURL } from '@/utils/misc'; import { getDirPath, getFilename } from '@/utils/path'; import { NativeFile, RemoteFile } from '@/utils/file'; -import { copyURIToPath } from '@/utils/bridge'; +import { copyURIToPath, getStorefrontRegionCode } from '@/utils/bridge'; import { copyFiles } from '@/utils/files'; import { BaseAppService } from './appService'; @@ -421,6 +421,8 @@ export class NativeAppService extends BaseAppService { override canCustomizeRootDir = DIST_CHANNEL !== 'appstore'; override canReadExternalDir = DIST_CHANNEL !== 'appstore' && DIST_CHANNEL !== 'playstore'; override distChannel = DIST_CHANNEL; + override storefrontRegionCode: string | null = null; + override isOnlineCatalogsAccessible = true; private execDir?: string = undefined; @@ -446,6 +448,14 @@ export class NativeAppService extends BaseAppService { execDir, }); } + if (this.isIOSApp) { + const res = await getStorefrontRegionCode(); + if (res.regionCode) { + this.storefrontRegionCode = res.regionCode; + this.isOnlineCatalogsAccessible = + this.storefrontRegionCode.toLowerCase() !== 'chn' || this.distChannel !== 'appstore'; + } + } await this.prepareBooksDir(); await this.runMigrations(); } diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index 3edfbc35..b937e288 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -89,6 +89,8 @@ export interface AppService { canCustomizeRootDir: boolean; canReadExternalDir: boolean; distChannel: DistChannel; + storefrontRegionCode: string | null; + isOnlineCatalogsAccessible: boolean; init(): Promise; openFile(path: string, base: BaseDir): Promise; diff --git a/apps/readest-app/src/utils/bridge.ts b/apps/readest-app/src/utils/bridge.ts index cec7068d..222bc502 100644 --- a/apps/readest-app/src/utils/bridge.ts +++ b/apps/readest-app/src/utils/bridge.ts @@ -91,6 +91,11 @@ interface SelectDirectoryResponse { error?: string; } +export interface GetStorefrontRegionCodeResponse { + regionCode?: string; + error?: string; +} + export async function copyURIToPath(request: CopyURIRequest): Promise { const result = await invoke('plugin:native-bridge|copy_uri_to_path', { payload: request, @@ -202,3 +207,10 @@ export async function selectDirectory(): Promise { const result = await invoke('plugin:native-bridge|select_directory'); return result; } + +export async function getStorefrontRegionCode(): Promise { + const result = await invoke( + 'plugin:native-bridge|get_storefront_region_code', + ); + return result; +}