forked from akai/readest
chore(opds): disable popular online opds catalogs in certain regions on App Store (#3031)
This commit is contained in:
@@ -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",
|
||||
|
||||
+10
@@ -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")
|
||||
|
||||
+13
@@ -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"]
|
||||
+27
@@ -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.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-storefront-region-code`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_storefront_region_code command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-get-storefront-region-code`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_storefront_region_code command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-sys-fonts-list`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -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",
|
||||
|
||||
+14
-2
@@ -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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -186,6 +186,13 @@ pub(crate) async fn select_directory<R: Runtime>(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_storefront_region_code<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<GetStorefrontRegionCodeResponse> {
|
||||
app.native_bridge().get_storefront_region_code()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn request_manage_storage_permission<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
|
||||
@@ -137,6 +137,10 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn get_storefront_region_code(&self) -> crate::Result<GetStorefrontRegionCodeResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
) -> crate::Result<RequestManageStoragePermissionResponse> {
|
||||
|
||||
@@ -77,6 +77,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
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| {
|
||||
|
||||
@@ -224,6 +224,14 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_storefront_region_code(&self) -> crate::Result<GetStorefrontRegionCodeResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_storefront_region_code", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
|
||||
@@ -230,3 +230,10 @@ pub struct SelectDirectoryResponse {
|
||||
pub path: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetStorefrontRegionCodeResponse {
|
||||
pub region_code: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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<ImportMenuProps> = ({
|
||||
onOpenCatalogManager,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
|
||||
const handleImportFromFiles = () => {
|
||||
onImportBooksFromFiles();
|
||||
@@ -54,7 +55,7 @@ const ImportMenu: React.FC<ImportMenuProps> = ({
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Online Library')}
|
||||
label={appService?.isOnlineCatalogsAccessible ? _('Online Library') : _('OPDS Catalogs')}
|
||||
Icon={<MdRssFeed className='h-5 w-5' />}
|
||||
onClick={handleOpenCatalogManager}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={_('Online Library')}
|
||||
title={appService?.isOnlineCatalogsAccessible ? _('Online Library') : _('OPDS Catalogs')}
|
||||
onClose={onClose}
|
||||
bgClassName={'sm:!bg-black/75'}
|
||||
boxClassName='sm:min-w-[520px] sm:w-3/4 sm:h-[85%] sm:!max-w-screen-sm'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { IoAdd, IoTrash, IoOpenOutline, IoBook, IoEyeOff, IoEye } from 'react-icons/io5';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -56,7 +57,7 @@ async function validateOPDSCatalog(
|
||||
export function CatalogManager() {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const { envConfig } = useEnv();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const [catalogs, setCatalogs] = useState<OPDSCatalog[]>(() => 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() {
|
||||
</section>
|
||||
|
||||
{/* Popular Catalogs */}
|
||||
<section className='text-base'>
|
||||
<section className={clsx('text-base', popularCatalogs.length === 0 && 'hidden')}>
|
||||
<h2 className='mb-4 font-semibold'>{_('Popular Catalogs')}</h2>
|
||||
<div className='grid gap-4 sm:grid-cols-2'>
|
||||
{POPULAR_CATALOGS.filter((catalog) => !catalog.disabled).map((catalog) => {
|
||||
const isAdded = catalogs.some((c) => c.url === catalog.url);
|
||||
return (
|
||||
<div
|
||||
key={catalog.id}
|
||||
className='card bg-base-100 border-base-300 border shadow-sm transition-shadow hover:shadow-md'
|
||||
>
|
||||
<div className='card-body p-4'>
|
||||
<h3 className='card-title mb-1 text-sm'>
|
||||
{catalog.icon && <span className=''>{catalog.icon}</span>}
|
||||
{catalog.name}
|
||||
</h3>
|
||||
{catalog.description && (
|
||||
<p className='text-base-content/70 line-clamp-2 text-sm'>
|
||||
{catalog.description}
|
||||
</p>
|
||||
)}
|
||||
<div className='card-actions mt-4 justify-end gap-2'>
|
||||
{!isAdded && (
|
||||
<button
|
||||
onClick={() => handleAddPopularCatalog(catalog)}
|
||||
className='btn btn-sm'
|
||||
>
|
||||
<IoAdd className='h-4 w-4' />
|
||||
{_('Add')}
|
||||
</button>
|
||||
{popularCatalogs
|
||||
.filter((catalog) => !catalog.disabled)
|
||||
.map((catalog) => {
|
||||
const isAdded = catalogs.some((c) => c.url === catalog.url);
|
||||
return (
|
||||
<div
|
||||
key={catalog.id}
|
||||
className='card bg-base-100 border-base-300 border shadow-sm transition-shadow hover:shadow-md'
|
||||
>
|
||||
<div className='card-body p-4'>
|
||||
<h3 className='card-title mb-1 text-sm'>
|
||||
{catalog.icon && <span className=''>{catalog.icon}</span>}
|
||||
{catalog.name}
|
||||
</h3>
|
||||
{catalog.description && (
|
||||
<p className='text-base-content/70 line-clamp-2 text-sm'>
|
||||
{catalog.description}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleOpenCatalog(catalog)}
|
||||
className='btn btn-sm btn-primary'
|
||||
>
|
||||
<IoOpenOutline className='h-4 w-4' />
|
||||
{_('Browse')}
|
||||
</button>
|
||||
<div className='card-actions mt-4 justify-end gap-2'>
|
||||
{!isAdded && (
|
||||
<button
|
||||
onClick={() => handleAddPopularCatalog(catalog)}
|
||||
className='btn btn-sm'
|
||||
>
|
||||
<IoAdd className='h-4 w-4' />
|
||||
{_('Add')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleOpenCatalog(catalog)}
|
||||
className='btn btn-sm btn-primary'
|
||||
>
|
||||
<IoOpenOutline className='h-4 w-4' />
|
||||
{_('Browse')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ export interface AppService {
|
||||
canCustomizeRootDir: boolean;
|
||||
canReadExternalDir: boolean;
|
||||
distChannel: DistChannel;
|
||||
storefrontRegionCode: string | null;
|
||||
isOnlineCatalogsAccessible: boolean;
|
||||
|
||||
init(): Promise<void>;
|
||||
openFile(path: string, base: BaseDir): Promise<File>;
|
||||
|
||||
@@ -91,6 +91,11 @@ interface SelectDirectoryResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface GetStorefrontRegionCodeResponse {
|
||||
regionCode?: 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,
|
||||
@@ -202,3 +207,10 @@ export async function selectDirectory(): Promise<SelectDirectoryResponse> {
|
||||
const result = await invoke<SelectDirectoryResponse>('plugin:native-bridge|select_directory');
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getStorefrontRegionCode(): Promise<GetStorefrontRegionCodeResponse> {
|
||||
const result = await invoke<GetStorefrontRegionCodeResponse>(
|
||||
'plugin:native-bridge|get_storefront_region_code',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user