feat: supported changing data location on desktop platforms, closes #2047 and closes #478 (#2125)

This commit is contained in:
Huang Xin
2025-09-26 16:22:41 +08:00
committed by GitHub
parent 9aa6a8e6b9
commit 1843a74dbb
39 changed files with 1197 additions and 193 deletions
Generated
+56 -115
View File
@@ -29,7 +29,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-build 2.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-build",
"tauri-plugin-cli",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
@@ -56,9 +56,9 @@ dependencies = [
[[package]]
name = "addr2line"
version = "0.24.2"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
@@ -310,7 +310,7 @@ dependencies = [
"polling",
"rustix",
"slab",
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -368,7 +368,7 @@ dependencies = [
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -425,9 +425,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "backtrace"
version = "0.3.75"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line",
"cfg-if",
@@ -435,7 +435,7 @@ dependencies = [
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
"windows-link 0.2.0",
]
[[package]]
@@ -1217,7 +1217,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -1455,7 +1455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -1916,9 +1916,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.31.1"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "gio"
@@ -2283,7 +2283,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.62.0",
"windows-core 0.62.1",
]
[[package]]
@@ -2818,9 +2818,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.7.5"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "memoffset"
@@ -3309,9 +3309,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.7"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
@@ -3791,7 +3791,7 @@ dependencies = [
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -4269,9 +4269,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.2"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
dependencies = [
"aho-corasick",
"memchr",
@@ -4281,9 +4281,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [
"aho-corasick",
"memchr",
@@ -4479,7 +4479,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -4565,7 +4565,7 @@ version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -4696,9 +4696,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.226"
version = "1.0.227"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245"
dependencies = [
"serde_core",
"serde_derive",
@@ -4718,18 +4718,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.226"
version = "1.0.227"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.226"
version = "1.0.227"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04"
dependencies = [
"proc-macro2",
"quote",
@@ -5327,11 +5327,11 @@ dependencies = [
"specta",
"swift-rs",
"tauri",
"tauri-build 2.4.1",
"tauri-build",
"tauri-macros",
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils 2.7.0",
"tauri-utils",
"thiserror 2.0.16",
"tokio",
"tracing",
@@ -5361,29 +5361,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-codegen",
"tauri-utils 2.7.0",
"tauri-winres",
"toml 0.9.7",
"walkdir",
]
[[package]]
name = "tauri-build"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
dependencies = [
"anyhow",
"cargo_toml",
"dirs",
"glob",
"heck 0.5.0",
"json-patch",
"schemars 0.8.22",
"semver",
"serde",
"serde_json",
"tauri-utils 2.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils",
"tauri-winres",
"toml 0.9.7",
"walkdir",
@@ -5406,7 +5384,7 @@ dependencies = [
"serde_json",
"sha2",
"syn 2.0.106",
"tauri-utils 2.7.0",
"tauri-utils",
"thiserror 2.0.16",
"time",
"url",
@@ -5423,7 +5401,7 @@ dependencies = [
"quote",
"syn 2.0.106",
"tauri-codegen",
"tauri-utils 2.7.0",
"tauri-utils",
]
[[package]]
@@ -5438,7 +5416,7 @@ dependencies = [
"schemars 0.8.22",
"serde",
"serde_json",
"tauri-utils 2.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils",
"toml 0.9.7",
"walkdir",
]
@@ -5471,7 +5449,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils 2.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils",
"thiserror 2.0.16",
"tracing",
"url",
@@ -5513,7 +5491,7 @@ dependencies = [
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils 2.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils",
"thiserror 2.0.16",
"toml 0.9.7",
"url",
@@ -5793,7 +5771,7 @@ dependencies = [
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils 2.7.0",
"tauri-utils",
"thiserror 2.0.16",
"url",
"webkit2gtk",
@@ -5818,7 +5796,7 @@ dependencies = [
"softbuffer",
"tao",
"tauri-runtime",
"tauri-utils 2.7.0",
"tauri-utils",
"tracing",
"url",
"webkit2gtk",
@@ -5868,43 +5846,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-utils"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
dependencies = [
"anyhow",
"cargo_metadata",
"ctor",
"dunce",
"glob",
"html5ever",
"http",
"infer",
"json-patch",
"kuchikiki",
"log",
"memchr",
"phf 0.11.3",
"proc-macro2",
"quote",
"regex",
"schemars 0.8.22",
"semver",
"serde",
"serde-untagged",
"serde_json",
"serde_with",
"swift-rs",
"thiserror 2.0.16",
"toml 0.9.7",
"url",
"urlpattern",
"uuid",
"walkdir",
]
[[package]]
name = "tauri-winres"
version = "0.3.3"
@@ -5925,7 +5866,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -6853,7 +6794,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -6914,9 +6855,9 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.62.0"
version = "0.62.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
dependencies = [
"windows-implement",
"windows-interface",
@@ -6938,9 +6879,9 @@ dependencies = [
[[package]]
name = "windows-implement"
version = "0.60.0"
version = "0.60.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0"
dependencies = [
"proc-macro2",
"quote",
@@ -6949,9 +6890,9 @@ dependencies = [
[[package]]
name = "windows-interface"
version = "0.59.1"
version = "0.59.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5"
dependencies = [
"proc-macro2",
"quote",
@@ -7060,14 +7001,14 @@ version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.3",
"windows-targets 0.53.4",
]
[[package]]
name = "windows-sys"
version = "0.61.0"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
dependencies = [
"windows-link 0.2.0",
]
@@ -7105,11 +7046,11 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.53.3"
version = "0.53.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
dependencies = [
"windows-link 0.1.3",
"windows-link 0.2.0",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
@@ -7131,9 +7072,9 @@ dependencies = [
[[package]]
name = "windows-version"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e061eb0a22b4a1d778ad70f7575ec7845490abb35b08fa320df7895882cacb"
checksum = "700dad7c058606087f6fdc1f88da5841e06da40334413c6cd4367b25ef26d24e"
dependencies = [
"windows-link 0.2.0",
]
+4
View File
@@ -2,6 +2,8 @@
members = [
"apps/readest-app/src-tauri",
"packages/tauri/crates/tauri",
"packages/tauri/crates/tauri-utils",
"packages/tauri/crates/tauri-build",
]
resolver = "2"
@@ -31,3 +33,5 @@ rust-version = "1.77.2"
[patch.crates-io]
tauri = { path = "packages/tauri/crates/tauri" }
tauri-utils = { path = "packages/tauri/crates/tauri-utils" }
tauri-build = { path = "packages/tauri/crates/tauri-build" }
@@ -590,5 +590,27 @@
"Single Page": "صفحة واحدة",
"Auto Spread": "انتشار تلقائي",
"Fit Page": "تناسب الصفحة",
"Fit Width": "تناسب العرض"
"Fit Width": "تناسب العرض",
"Failed to select directory": "فشل في اختيار الدليل",
"The new data directory must be different from the current one.": "يجب أن يكون دليل البيانات الجديد مختلفًا عن الدليل الحالي.",
"Migration failed: {{error}}": "فشل في الترحيل: {{error}}",
"Change Data Location": "تغيير موقع البيانات",
"Current Data Location": "موقع البيانات الحالي",
"Total size: {{size}}": "الحجم الكلي: {{size}}",
"Calculating file info...": "جارٍ حساب معلومات الملف...",
"New Data Location": "موقع البيانات الجديد",
"Choose Different Folder": "اختر مجلدًا مختلفًا",
"Choose New Folder": "اختر مجلدًا جديدًا",
"Migrating data...": "جارٍ ترحيل البيانات...",
"Copying: {{file}}": "جارٍ نسخ: {{file}}",
"{{current}} of {{total}} files": "{{current}} من {{total}} ملف",
"Migration completed successfully!": "اكتمل الترحيل بنجاح!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "تم نقل بياناتك إلى الموقع الجديد. يرجى إعادة تشغيل التطبيق لإكمال العملية.",
"Migration failed": "فشل في الترحيل",
"Important Notice": "إشعار مهم",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "سيؤدي ذلك إلى نقل جميع بيانات التطبيق الخاصة بك إلى الموقع الجديد. تأكد من أن الوجهة تحتوي على مساحة خالية كافية.",
"Restart App": "إعادة تشغيل التطبيق",
"Start Migration": "بدء الترحيل",
"Advanced Settings": "إعدادات متقدمة",
"File count: {{size}}": "عدد الملفات: {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "একক পৃষ্ঠা",
"Auto Spread": "অটো স্প্রেড",
"Fit Page": "ফিট পৃষ্ঠা",
"Fit Width": "ফিট প্রস্থ"
"Fit Width": "ফিট প্রস্থ",
"Failed to select directory": "ডিরেক্টরি নির্বাচন করতে ব্যর্থ",
"The new data directory must be different from the current one.": "নতুন ডেটা ডিরেক্টরি বর্তমানটির থেকে আলাদা হতে হবে।",
"Migration failed: {{error}}": "মাইগ্রেশন ব্যর্থ হয়েছে: {{error}}",
"Change Data Location": "ডেটা অবস্থান পরিবর্তন করুন",
"Current Data Location": "বর্তমান ডেটা অবস্থান",
"Total size: {{size}}": "মোট আকার: {{size}}",
"Calculating file info...": "ফাইলের তথ্য গণনা করা হচ্ছে...",
"New Data Location": "নতুন ডেটা অবস্থান",
"Choose Different Folder": "ভিন্ন ফোল্ডার নির্বাচন করুন",
"Choose New Folder": "নতুন ফোল্ডার নির্বাচন করুন",
"Migrating data...": "ডেটা স্থানান্তরিত হচ্ছে...",
"Copying: {{file}}": "কপি করা হচ্ছে: {{file}}",
"{{current}} of {{total}} files": "{{current}} এর {{total}} ফাইল",
"Migration completed successfully!": "মাইগ্রেশন সফলভাবে সম্পন্ন হয়েছে!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "আপনার ডেটা নতুন অবস্থানে স্থানান্তরিত হয়েছে। প্রক্রিয়া সম্পন্ন করতে দয়া করে অ্যাপ্লিকেশনটি পুনরায় চালু করুন।",
"Migration failed": "মাইগ্রেশন ব্যর্থ হয়েছে",
"Important Notice": "গুরুতর বিজ্ঞপ্তি",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "এটি আপনার সমস্ত অ্যাপ ডেটা নতুন অবস্থানে স্থানান্তরিত করবে। নিশ্চিত করুন যে গন্তব্যে পর্যাপ্ত ফ্রি স্পেস রয়েছে।",
"Restart App": "অ্যাপ পুনরায় চালু করুন",
"Start Migration": "মাইগ্রেশন শুরু করুন",
"Advanced Settings": "উন্নত সেটিংস",
"File count: {{size}}": "ফাইলের সংখ্যা: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "དེབ་གཅིག",
"Auto Spread": "རང་འགུལ་སྤྲོད།",
"Fit Page": "དེབ་འདེམས་པ།",
"Fit Width": "ཁྱབ་སྒྲིག་འདེམས་པ།"
"Fit Width": "ཁྱབ་སྒྲིག་འདེམས་པ།",
"Failed to select directory": "གནས་ས་འདེམས་པ་བྱས་མ་ཐུབ།",
"The new data directory must be different from the current one.": "གསར་བཅས་སྐོར་འདེམས་པ་གནས་ས་དང་མཉམ་འབྱུང་བར་བྱས་མ་ཐུབ།",
"Migration failed: {{error}}": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
"Change Data Location": "གསར་བཅས་སྐོར་འདེམས་པ།",
"Current Data Location": "ད་ལྟ་བཅས་སྐོར་འདེམས་པ།",
"Total size: {{size}}": "དངོས་གནས་བཅས་: {{size}}",
"Calculating file info...": "དེབ་གནས་བཅས་པའི་གདམ་ཁ་བཟོ་བྱས་མ་ཐུབ།",
"New Data Location": "གསར་བཅས་སྐོར་འདེམས་པ།",
"Choose Different Folder": "གཞན་དེབ་སྣོད་བཀར་བ།",
"Choose New Folder": "གསར་བཅས་སྣོད་བཀར་བ།",
"Migrating data...": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Copying: {{file}}": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"{{current}} of {{total}} files": "{{current}} དང་ {{total}} དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Migration completed successfully!": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
"Your data has been moved to the new location. Please restart the application to complete the process.": "ཁྱོད་ཀྱི་དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Migration failed": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
"Important Notice": "དོན་གཞི་བརྗེ་བ།",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "འདི་ནས་ཁྱོད་ཀྱི་ཨང་གཏོད་དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Restart App": "དེབ་ཀྱི་སྤྱོད་བྱས་མ་ཐུབ།",
"Start Migration": "མཉམ་དུ་ཀློག་འཛུལ་བྱས་མ་ཐུབ།",
"Advanced Settings": "དབང་བསྐྱོད་སྒྲིག་སྟངས།",
"File count: {{size}}": "དེབ་གནས་བཅས་པའི་ཨང་། {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "Einzelne Seite",
"Auto Spread": "Automatische Verbreitung",
"Fit Page": "Seite anpassen",
"Fit Width": "Breite anpassen"
"Fit Width": "Breite anpassen",
"Failed to select directory": "Fehler beim Auswählen des Verzeichnisses",
"The new data directory must be different from the current one.": "Das neue Datenverzeichnis muss sich vom aktuellen unterscheiden.",
"Migration failed: {{error}}": "Migration fehlgeschlagen: {{error}}",
"Change Data Location": "Datenstandort ändern",
"Current Data Location": "Aktueller Datenstandort",
"Total size: {{size}}": "Gesamtgröße: {{size}}",
"Calculating file info...": "Dateiinformationen werden berechnet...",
"New Data Location": "Neuer Datenstandort",
"Choose Different Folder": "Anderen Ordner wählen",
"Choose New Folder": "Neuen Ordner wählen",
"Migrating data...": "Daten werden migriert...",
"Copying: {{file}}": "Kopiere: {{file}}",
"{{current}} of {{total}} files": "{{current}} von {{total}} Dateien",
"Migration completed successfully!": "Migration erfolgreich abgeschlossen!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ihre Daten wurden an den neuen Speicherort verschoben. Bitte starten Sie die Anwendung neu, um den Vorgang abzuschließen.",
"Migration failed": "Migration fehlgeschlagen",
"Important Notice": "Wichtiger Hinweis",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Dies wird alle Ihre App-Daten an den neuen Speicherort verschieben. Stellen Sie sicher, dass das Ziel über ausreichend freien Speicherplatz verfügt.",
"Restart App": "App neu starten",
"Start Migration": "Migration starten",
"Advanced Settings": "Erweiterte Einstellungen",
"File count: {{size}}": "Dateianzahl: {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "Μοναδική σελίδα",
"Auto Spread": "Αυτόματη εξάπλωση",
"Fit Page": "Προσαρμογή σελίδας",
"Fit Width": "Προσαρμογή πλάτους"
"Fit Width": "Προσαρμογή πλάτους",
"Failed to select directory": "Αποτυχία επιλογής καταλόγου",
"The new data directory must be different from the current one.": "Ο νέος κατάλογος δεδομένων πρέπει να είναι διαφορετικός από τον τρέχοντα.",
"Migration failed: {{error}}": "Η μετανάστευση απέτυχε: {{error}}",
"Change Data Location": "Αλλαγή τοποθεσίας δεδομένων",
"Current Data Location": "Τρέχουσα τοποθεσία δεδομένων",
"Total size: {{size}}": "Συνολικό μέγεθος: {{size}}",
"Calculating file info...": "Υπολογισμός πληροφοριών αρχείου...",
"New Data Location": "Νέα τοποθεσία δεδομένων",
"Choose Different Folder": "Επιλέξτε διαφορετικό φάκελο",
"Choose New Folder": "Επιλέξτε νέο φάκελο",
"Migrating data...": "Μεταφορά δεδομένων...",
"Copying: {{file}}": "Αντιγραφή: {{file}}",
"{{current}} of {{total}} files": "{{current}} από {{total}} αρχεία",
"Migration completed successfully!": "Η μετανάστευση ολοκληρώθηκε με επιτυχία!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Τα δεδομένα σας έχουν μεταφερθεί στη νέα τοποθεσία. Παρακαλώ επανεκκινήστε την εφαρμογή για να ολοκληρώσετε τη διαδικασία.",
"Migration failed": "Η μετανάστευση απέτυχε",
"Important Notice": "Σημαντική ειδοποίηση",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Αυτό θα μεταφέρει όλα τα δεδομένα της εφαρμογής σας στη νέα τοποθεσία. Βεβαιωθείτε ότι ο προορισμός έχει αρκετό ελεύθερο χώρο.",
"Restart App": "Επανεκκίνηση εφαρμογής",
"Start Migration": "Έναρξη μετανάστευσης",
"Advanced Settings": "Προηγμένες ρυθμίσεις",
"File count: {{size}}": "Αριθμός αρχείων: {{size}}"
}
@@ -578,5 +578,27 @@
"Single Page": "Página Única",
"Auto Spread": "Distribución Automática",
"Fit Page": "Ajustar Página",
"Fit Width": "Ajustar Ancho"
"Fit Width": "Ajustar Ancho",
"Failed to select directory": "Error al seleccionar el directorio",
"The new data directory must be different from the current one.": "El nuevo directorio de datos debe ser diferente del actual.",
"Migration failed: {{error}}": "La migración falló: {{error}}",
"Change Data Location": "Cambiar Ubicación de Datos",
"Current Data Location": "Ubicación Actual de Datos",
"Total size: {{size}}": "Tamaño total: {{size}}",
"Calculating file info...": "Calculando información del archivo...",
"New Data Location": "Nueva Ubicación de Datos",
"Choose Different Folder": "Elegir Carpeta Diferente",
"Choose New Folder": "Elegir Nueva Carpeta",
"Migrating data...": "Migrando datos...",
"Copying: {{file}}": "Copiando: {{file}}",
"{{current}} of {{total}} files": "{{current}} de {{total}} archivos",
"Migration completed successfully!": "¡Migración completada con éxito!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Tus datos han sido movidos a la nueva ubicación. Por favor, reinicia la aplicación para completar el proceso.",
"Migration failed": "La migración falló",
"Important Notice": "Aviso Importante",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Esto moverá todos los datos de tu aplicación a la nueva ubicación. Asegúrate de que el destino tenga suficiente espacio libre.",
"Restart App": "Reiniciar Aplicación",
"Start Migration": "Iniciar Migración",
"Advanced Settings": "Configuraciones Avanzadas",
"File count: {{size}}": "Número de archivos: {{size}}"
}
@@ -578,5 +578,27 @@
"Single Page": "Page Unique",
"Auto Spread": "Répartition Automatique",
"Fit Page": "Ajuster la Page",
"Fit Width": "Ajuster la Largeur"
"Fit Width": "Ajuster la Largeur",
"Failed to select directory": "Échec de la sélection du répertoire",
"The new data directory must be different from the current one.": "Le nouveau répertoire de données doit être différent de l'actuel.",
"Migration failed: {{error}}": "La migration a échoué : {{error}}",
"Change Data Location": "Changer l'Emplacement des Données",
"Current Data Location": "Emplacement Actuel des Données",
"Total size: {{size}}": "Taille totale : {{size}}",
"Calculating file info...": "Calcul des informations sur le fichier...",
"New Data Location": "Nouvel Emplacement des Données",
"Choose Different Folder": "Choisir un Dossier Différent",
"Choose New Folder": "Choisir un Nouveau Dossier",
"Migrating data...": "Migration des données...",
"Copying: {{file}}": "Copie : {{file}}",
"{{current}} of {{total}} files": "{{current}} sur {{total}} fichiers",
"Migration completed successfully!": "Migration terminée avec succès !",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Vos données ont été déplacées vers le nouvel emplacement. Veuillez redémarrer l'application pour terminer le processus.",
"Migration failed": "La migration a échoué",
"Important Notice": "Avis Important",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Cela déplacera toutes vos données d'application vers le nouvel emplacement. Assurez-vous que la destination dispose de suffisamment d'espace libre.",
"Restart App": "Redémarrer l'Application",
"Start Migration": "Démarrer la Migration",
"Advanced Settings": "Paramètres Avancés",
"File count: {{size}}": "Nombre de fichiers : {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "एकल पृष्ठ",
"Auto Spread": "स्वचालित फैलाव",
"Fit Page": "पृष्ठ में फिट करें",
"Fit Width": "चौड़ाई में फिट करें"
"Fit Width": "चौड़ाई में फिट करें",
"Failed to select directory": "निर्देशिका का चयन करने में विफल",
"The new data directory must be different from the current one.": "नया डेटा निर्देशिका वर्तमान से भिन्न होना चाहिए।",
"Migration failed: {{error}}": "माइग्रेशन विफल: {{error}}",
"Change Data Location": "डेटा स्थान बदलें",
"Current Data Location": "वर्तमान डेटा स्थान",
"Total size: {{size}}": "कुल आकार: {{size}}",
"Calculating file info...": "फ़ाइल जानकारी की गणना कर रहे हैं...",
"New Data Location": "नया डेटा स्थान",
"Choose Different Folder": "विभिन्न फ़ोल्डर चुनें",
"Choose New Folder": "नया फ़ोल्डर चुनें",
"Migrating data...": "डेटा माइग्रेट कर रहे हैं...",
"Copying: {{file}}": "कॉपी कर रहे हैं: {{file}}",
"{{current}} of {{total}} files": "{{current}} में से {{total}} फ़ाइलें",
"Migration completed successfully!": "माइग्रेशन सफलतापूर्वक पूरा हुआ!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "आपका डेटा नए स्थान पर स्थानांतरित कर दिया गया है। कृपया प्रक्रिया को पूरा करने के लिए एप्लिकेशन को पुनरारंभ करें।",
"Migration failed": "माइग्रेशन विफल",
"Important Notice": "महत्वपूर्ण सूचना",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "यह आपके सभी ऐप डेटा को नए स्थान पर स्थानांतरित कर देगा। सुनिश्चित करें कि गंतव्य में पर्याप्त खाली स्थान है।",
"Restart App": "एप्लिकेशन पुनरारंभ करें",
"Start Migration": "माइग्रेशन प्रारंभ करें",
"Advanced Settings": "उन्नत सेटिंग्स",
"File count: {{size}}": "फ़ाइलों की संख्या: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "Halaman Tunggal",
"Auto Spread": "Penyebaran Otomatis",
"Fit Page": "Sesuaikan Halaman",
"Fit Width": "Sesuaikan Lebar"
"Fit Width": "Sesuaikan Lebar",
"Failed to select directory": "Gagal memilih direktori",
"The new data directory must be different from the current one.": "Direktori data baru harus berbeda dari yang saat ini.",
"Migration failed: {{error}}": "Migrasi gagal: {{error}}",
"Change Data Location": "Ubah Lokasi Data",
"Current Data Location": "Lokasi Data Saat Ini",
"Total size: {{size}}": "Ukuran total: {{size}}",
"Calculating file info...": "Menghitung informasi file...",
"New Data Location": "Lokasi Data Baru",
"Choose Different Folder": "Pilih Folder Berbeda",
"Choose New Folder": "Pilih Folder Baru",
"Migrating data...": "Migrasi data...",
"Copying: {{file}}": "Menyalin: {{file}}",
"{{current}} of {{total}} files": "{{current}} dari {{total}} file",
"Migration completed successfully!": "Migrasi selesai dengan sukses!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Data Anda telah dipindahkan ke lokasi baru. Silakan restart aplikasi untuk menyelesaikan proses.",
"Migration failed": "Migrasi gagal",
"Important Notice": "Pemberitahuan Penting",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Ini akan memindahkan semua data aplikasi Anda ke lokasi baru. Pastikan tujuan memiliki cukup ruang kosong.",
"Restart App": "Restart Aplikasi",
"Start Migration": "Mulai Migrasi",
"Advanced Settings": "Pengaturan Lanjutan",
"File count: {{size}}": "Jumlah file: {{size}}"
}
@@ -578,5 +578,27 @@
"Single Page": "Pagina singola",
"Auto Spread": "Distribuzione automatica",
"Fit Page": "Adatta pagina",
"Fit Width": "Adatta larghezza"
"Fit Width": "Adatta larghezza",
"Failed to select directory": "Impossibile selezionare la directory",
"The new data directory must be different from the current one.": "La nuova directory dei dati deve essere diversa da quella attuale.",
"Migration failed: {{error}}": "Migrzione fallita: {{error}}",
"Change Data Location": "Cambia posizione dei dati",
"Current Data Location": "Posizione attuale dei dati",
"Total size: {{size}}": "Dimensione totale: {{size}}",
"Calculating file info...": "Calcolo informazioni file...",
"New Data Location": "Nuova posizione dei dati",
"Choose Different Folder": "Scegli una cartella diversa",
"Choose New Folder": "Scegli una nuova cartella",
"Migrating data...": "Migrando dati...",
"Copying: {{file}}": "Copia in corso: {{file}}",
"{{current}} of {{total}} files": "{{current}} di {{total}} file",
"Migration completed successfully!": "Migrzione completata con successo!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "I tuoi dati sono stati spostati nella nuova posizione. Riavvia l'applicazione per completare il processo.",
"Migration failed": "Migrzione fallita",
"Important Notice": "Avviso Importante",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Questo sposterà tutti i dati dell'app nella nuova posizione. Assicurati che la destinazione abbia spazio libero sufficiente.",
"Restart App": "Riavvia App",
"Start Migration": "Inizia Migrazione",
"Advanced Settings": "Impostazioni Avanzate",
"File count: {{size}}": "Conteggio file: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "シングルページ",
"Auto Spread": "自動スプレッド",
"Fit Page": "ページに合わせる",
"Fit Width": "幅に合わせる"
"Fit Width": "幅に合わせる",
"Failed to select directory": "ディレクトリの選択に失敗しました",
"The new data directory must be different from the current one.": "新しいデータディレクトリは現在のものと異なる必要があります。",
"Migration failed: {{error}}": "移行に失敗しました: {{error}}",
"Change Data Location": "データの場所を変更",
"Current Data Location": "現在のデータの場所",
"Total size: {{size}}": "合計サイズ: {{size}}",
"Calculating file info...": "ファイル情報を計算中...",
"New Data Location": "新しいデータの場所",
"Choose Different Folder": "別のフォルダーを選択",
"Choose New Folder": "新しいフォルダーを選択",
"Migrating data...": "データを移行中...",
"Copying: {{file}}": "コピー中: {{file}}",
"{{current}} of {{total}} files": "{{current}} / {{total}} ファイル",
"Migration completed successfully!": "移行が成功しました!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "データは新しい場所に移動されました。プロセスを完了するにはアプリケーションを再起動してください。",
"Migration failed": "移行に失敗しました",
"Important Notice": "重要なお知らせ",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "これにより、すべてのアプリデータが新しい場所に移動されます。宛先に十分な空き容量があることを確認してください。",
"Restart App": "アプリを再起動",
"Start Migration": "移行を開始",
"Advanced Settings": "詳細設定",
"File count: {{size}}": "ファイル数: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "단일 페이지",
"Auto Spread": "자동 스프레드",
"Fit Page": "페이지에 맞춤",
"Fit Width": "너비에 맞춤"
"Fit Width": "너비에 맞춤",
"Failed to select directory": "디렉토리 선택 실패",
"The new data directory must be different from the current one.": "새 데이터 디렉토리는 현재 디렉토리와 달라야 합니다.",
"Migration failed: {{error}}": "마이그레이션 실패: {{error}}",
"Change Data Location": "데이터 위치 변경",
"Current Data Location": "현재 데이터 위치",
"Total size: {{size}}": "총 크기: {{size}}",
"Calculating file info...": "파일 정보 계산 중...",
"New Data Location": "새 데이터 위치",
"Choose Different Folder": "다른 폴더 선택",
"Choose New Folder": "새 폴더 선택",
"Migrating data...": "데이터 마이그레이션 중...",
"Copying: {{file}}": "복사 중: {{file}}",
"{{current}} of {{total}} files": "{{current}} / {{total}} 파일",
"Migration completed successfully!": "마이그레이션이 성공적으로 완료되었습니다!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "데이터가 새 위치로 이동되었습니다. 프로세스를 완료하려면 애플리케이션을 재시작하십시오.",
"Migration failed": "마이그레이션 실패",
"Important Notice": "중요한 공지",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "이 작업은 모든 앱 데이터를 새 위치로 이동합니다. 대상에 충분한 여유 공간이 있는지 확인하십시오.",
"Restart App": "앱 재시작",
"Start Migration": "마이그레이션 시작",
"Advanced Settings": "고급 설정",
"File count: {{size}}": "파일 수: {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "Enkele Pagina",
"Auto Spread": "Automatische Verspreiding",
"Fit Page": "Pagina Aanpassen",
"Fit Width": "Breedte Aanpassen"
"Fit Width": "Breedte Aanpassen",
"Failed to select directory": "Selecteren van map is mislukt",
"The new data directory must be different from the current one.": "De nieuwe gegevensmap moet anders zijn dan de huidige.",
"Migration failed: {{error}}": "Migratie mislukt: {{error}}",
"Change Data Location": "Gegevenslocatie Wijzigen",
"Current Data Location": "Huidige Gegevenslocatie",
"Total size: {{size}}": "Totale grootte: {{size}}",
"Calculating file info...": "Bestandinformatie wordt berekend...",
"New Data Location": "Nieuwe Gegevenslocatie",
"Choose Different Folder": "Kies Een Andere Map",
"Choose New Folder": "Kies Nieuwe Map",
"Migrating data...": "Gegevens worden gemigreerd...",
"Copying: {{file}}": "Kopiëren: {{file}}",
"{{current}} of {{total}} files": "{{current}} van {{total}} bestanden",
"Migration completed successfully!": "Migratie succesvol voltooid!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Uw gegevens zijn naar de nieuwe locatie verplaatst. Start de applicatie opnieuw op om het proces te voltooien.",
"Migration failed": "Migratie mislukt",
"Important Notice": "Belangrijke Kennisgeving",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Dit zal al uw app-gegevens naar de nieuwe locatie verplaatsen. Zorg ervoor dat de bestemming voldoende vrije ruimte heeft.",
"Restart App": "Herstart App",
"Start Migration": "Start Migratie",
"Advanced Settings": "Geavanceerde Instellingen",
"File count: {{size}}": "Bestandsaantal: {{size}}"
}
@@ -582,5 +582,27 @@
"Single Page": "Pojedyncza Strona",
"Auto Spread": "Automatyczne Rozprzestrzenianie",
"Fit Page": "Dopasuj Stronę",
"Fit Width": "Dopasuj Szerokość"
"Fit Width": "Dopasuj Szerokość",
"Failed to select directory": "Nie udało się wybrać katalogu",
"The new data directory must be different from the current one.": "Nowy katalog danych musi różnić się od obecnego.",
"Migration failed: {{error}}": "Migracja nie powiodła się: {{error}}",
"Change Data Location": "Zmień lokalizację danych",
"Current Data Location": "Bieżąca lokalizacja danych",
"Total size: {{size}}": "Całkowity rozmiar: {{size}}",
"Calculating file info...": "Obliczanie informacji o pliku...",
"New Data Location": "Nowa lokalizacja danych",
"Choose Different Folder": "Wybierz inny folder",
"Choose New Folder": "Wybierz nowy folder",
"Migrating data...": "Migracja danych...",
"Copying: {{file}}": "Kopiowanie: {{file}}",
"{{current}} of {{total}} files": "{{current}} z {{total}} plików",
"Migration completed successfully!": "Migracja zakończona pomyślnie!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Twoje dane zostały przeniesione do nowej lokalizacji. Proszę zrestartować aplikację, aby zakończyć proces.",
"Migration failed": "Migracja nie powiodła się",
"Important Notice": "Ważne powiadomienie",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "To przeniesie wszystkie dane aplikacji do nowej lokalizacji. Upewnij się, że docelowy folder ma wystarczająco dużo wolnego miejsca.",
"Restart App": "Zrestartuj aplikację",
"Start Migration": "Rozpocznij migrację",
"Advanced Settings": "Ustawienia zaawansowane",
"File count: {{size}}": "Liczba plików: {{size}}"
}
@@ -578,5 +578,27 @@
"Single Page": "Página Única",
"Auto Spread": "Espalhamento Automático",
"Fit Page": "Ajustar Página",
"Fit Width": "Ajustar Largura"
"Fit Width": "Ajustar Largura",
"Failed to select directory": "Falha ao selecionar diretório",
"The new data directory must be different from the current one.": "O novo diretório de dados deve ser diferente do atual.",
"Migration failed: {{error}}": "A migração falhou: {{error}}",
"Change Data Location": "Alterar Localização dos Dados",
"Current Data Location": "Localização Atual dos Dados",
"Total size: {{size}}": "Tamanho total: {{size}}",
"Calculating file info...": "Calculando informações do arquivo...",
"New Data Location": "Nova Localização dos Dados",
"Choose Different Folder": "Escolher Pasta Diferente",
"Choose New Folder": "Escolher Nova Pasta",
"Migrating data...": "Migrando dados...",
"Copying: {{file}}": "Copiando: {{file}}",
"{{current}} of {{total}} files": "{{current}} de {{total}} arquivos",
"Migration completed successfully!": "Migração concluída com sucesso!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Seus dados foram movidos para a nova localização. Reinicie o aplicativo para concluir o processo.",
"Migration failed": "Migração falhou",
"Important Notice": "Aviso Importante",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Isso moverá todos os dados do seu aplicativo para a nova localização. Certifique-se de que o destino tenha espaço livre suficiente.",
"Restart App": "Reiniciar Aplicativo",
"Start Migration": "Iniciar Migração",
"Advanced Settings": "Configurações Avançadas",
"File count: {{size}}": "Contagem de arquivos: {{size}}"
}
@@ -582,5 +582,27 @@
"Single Page": "Одна страница",
"Auto Spread": "Автоматическое растяжение",
"Fit Page": "Подогнать страницу",
"Fit Width": "Подогнать ширину"
"Fit Width": "Подогнать ширину",
"Failed to select directory": "Не удалось выбрать каталог",
"The new data directory must be different from the current one.": "Новая директория данных должна отличаться от текущей.",
"Migration failed: {{error}}": "Ошибка миграции: {{error}}",
"Change Data Location": "Изменить расположение данных",
"Current Data Location": "Текущее расположение данных",
"Total size: {{size}}": "Общий размер: {{size}}",
"Calculating file info...": "Вычисление информации о файле...",
"New Data Location": "Новое расположение данных",
"Choose Different Folder": "Выбрать другую папку",
"Choose New Folder": "Выбрать новую папку",
"Migrating data...": "Миграция данных...",
"Copying: {{file}}": "Копирование: {{file}}",
"{{current}} of {{total}} files": "{{current}} из {{total}} файлов",
"Migration completed successfully!": "Миграция завершена успешно!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ваши данные были перемещены в новое расположение. Пожалуйста, перезапустите приложение, чтобы завершить процесс.",
"Migration failed": "Ошибка миграции",
"Important Notice": "Важное уведомление",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Это переместит все ваши данные приложения в новое расположение. Убедитесь, что в целевом месте достаточно свободного места.",
"Restart App": "Перезапустить приложение",
"Start Migration": "Начать миграцию",
"Advanced Settings": "Расширенные настройки",
"File count: {{size}}": "Количество файлов: {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "එක් පිටුව",
"Auto Spread": "ස්වයංක්‍රීය ව්‍යාප්තිය",
"Fit Page": "පිටුවට ගැලපෙන ලෙස",
"Fit Width": "පැතිකඩට ගැලපෙන ලෙස"
"Fit Width": "පැතිකඩට ගැලපෙන ලෙස",
"Failed to select directory": "ෆෝල්ඩරය තෝරා ගැනීමට අසමත්",
"The new data directory must be different from the current one.": "නව දත්ත ෆෝල්ඩරය වර්තමාන ෆෝල්ඩරයෙන් වෙනස් විය යුතුය.",
"Migration failed: {{error}}": "මැතිවරණය අසමත් විය: {{error}}",
"Change Data Location": "දත්ත ස්ථානය වෙනස් කරන්න",
"Current Data Location": "වර්තමාන දත්ත ස්ථානය",
"Total size: {{size}}": "මුළු ප්‍රමාණය: {{size}}",
"Calculating file info...": "ගොනු තොරතුරු ගණනය කරමින්...",
"New Data Location": "නව දත්ත ස්ථානය",
"Choose Different Folder": "වෙනත් ෆෝල්ඩරයක් තෝරන්න",
"Choose New Folder": "නව ෆෝල්ඩරයක් තෝරන්න",
"Migrating data...": "දත්ත මැතිවරණය කරමින්...",
"Copying: {{file}}": "පිටපත් කරමින්: {{file}}",
"{{current}} of {{total}} files": "{{current}}/{{total}} ගොනු",
"Migration completed successfully!": "මැතිවරණය සාර්ථකව නිමවිය!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "ඔබගේ දත්ත නව ස්ථානයට මාරු කර ඇත. ක්‍රියාවලිය සම්පූර්ණ කිරීමට කරුණාකර යෙදුම නැවත ආරම්භ කරන්න.",
"Migration failed": "මැතිවරණය අසමත් විය",
"Important Notice": "මහත් අවධානයක්",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "මෙය ඔබගේ යෙදුම් දත්ත සියල්ල නව ස්ථානයට මාරු කරනු ඇත. ගමන් මාර්ගය ප්‍රමාණවත් නිදහස් ස්ථානයක් ඇති බවට සහතික වන්න.",
"Restart App": "යෙදුම නැවත ආරම්භ කරන්න",
"Start Migration": "මැතිවරණය ආරම්භ කරන්න",
"Advanced Settings": "උසස් සැකසුම්",
"File count: {{size}}": "ගොනු ගණන: {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "ஒரு பக்கம்",
"Auto Spread": "தானாக பரவல்",
"Fit Page": "பக்கத்திற்கு பொருந்தும்",
"Fit Width": "அகலத்திற்கு பொருந்தும்"
"Fit Width": "அகலத்திற்கு பொருந்தும்",
"Failed to select directory": "கோப்புறையைத் தேர்வுசெய்ய முடியவில்லை",
"The new data directory must be different from the current one.": "புதிய தரவுக் கோப்புறை தற்போதையதைப் போலவே இருக்கக்கூடாது.",
"Migration failed: {{error}}": "மொத்தமாக்கல் தோல்வியுற்றது: {{error}}",
"Change Data Location": "தரவுப் இடத்தை மாற்றவும்",
"Current Data Location": "தற்போதைய தரவுப் இடம்",
"Total size: {{size}}": "மொத்த அளவு: {{size}}",
"Calculating file info...": "கோப்பு தகவலை கணக்கிடுகிறது...",
"New Data Location": "புதிய தரவுப் இடம்",
"Choose Different Folder": "வேறுபட்ட கோப்புறையைத் தேர்ந்தெடுக்கவும்",
"Choose New Folder": "புதிய கோப்புறையைத் தேர்ந்தெடுக்கவும்",
"Migrating data...": "தரவை மாறுகிறது...",
"Copying: {{file}}": "பகுப்பாய்வு: {{file}}",
"{{current}} of {{total}} files": "{{current}}/{{total}} கோப்புகள்",
"Migration completed successfully!": "மொத்தமாக்கல் வெற்றிகரமாக முடிந்தது!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "உங்கள் தரவுகள் புதிய இடத்திற்கு மாறியுள்ளன. செயலியை மீண்டும் தொடங்கவும்.",
"Migration failed": "மொத்தமாக்கல் தோல்வியுற்றது",
"Important Notice": "முக்கிய அறிவிப்பு",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "இது உங்கள் செயலியின் அனைத்து தரவுகளையும் புதிய இடத்திற்கு நகர்த்தும். இலக்கு இடத்தில் போதுமான இலவச இடம் உள்ளதா என்பதை உறுதிப்படுத்தவும்.",
"Restart App": "செயலியை மீண்டும் தொடங்கவும்",
"Start Migration": "மொத்தமாக்கலை தொடங்கவும்",
"Advanced Settings": "மேம்பட்ட அமைப்புகள்",
"File count: {{size}}": "கோப்பு எண்ணிக்கை: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "หน้าเดียว",
"Auto Spread": "การกระจายอัตโนมัติ",
"Fit Page": "พอดีกับหน้า",
"Fit Width": "พอดีกับความกว้าง"
"Fit Width": "พอดีกับความกว้าง",
"Failed to select directory": "ไม่สามารถเลือกไดเรกทอรีได้",
"The new data directory must be different from the current one.": "ไดเรกทอรีข้อมูลใหม่จะต้องแตกต่างจากไดเรกทอรีปัจจุบัน",
"Migration failed: {{error}}": "การย้ายข้อมูลล้มเหลว: {{error}}",
"Change Data Location": "เปลี่ยนตำแหน่งข้อมูล",
"Current Data Location": "ตำแหน่งข้อมูลปัจจุบัน",
"Total size: {{size}}": "ขนาดรวม: {{size}}",
"Calculating file info...": "กำลังคำนวณข้อมูลไฟล์...",
"New Data Location": "ตำแหน่งข้อมูลใหม่",
"Choose Different Folder": "เลือกโฟลเดอร์ที่แตกต่าง",
"Choose New Folder": "เลือกโฟลเดอร์ใหม่",
"Migrating data...": "กำลังย้ายข้อมูล...",
"Copying: {{file}}": "กำลังคัดลอก: {{file}}",
"{{current}} of {{total}} files": "{{current}} จาก {{total}} ไฟล์",
"Migration completed successfully!": "การย้ายข้อมูลเสร็จสมบูรณ์!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "ข้อมูลของคุณได้ถูกย้ายไปยังตำแหน่งใหม่แล้ว กรุณารีสตาร์ทแอปพลิเคชันเพื่อทำให้กระบวนการเสร็จสมบูรณ์",
"Migration failed": "การย้ายข้อมูลล้มเหลว",
"Important Notice": "ประกาศสำคัญ",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "สิ่งนี้จะย้ายข้อมูลแอปทั้งหมดของคุณไปยังตำแหน่งใหม่ ตรวจสอบให้แน่ใจว่าตำแหน่งปลายทางมีพื้นที่ว่างเพียงพอ",
"Restart App": "รีสตาร์ทแอป",
"Start Migration": "เริ่มการย้ายข้อมูล",
"Advanced Settings": "การตั้งค่าขั้นสูง",
"File count: {{size}}": "จำนวนไฟล์: {{size}}"
}
@@ -574,5 +574,27 @@
"Single Page": "Tek Sayfa",
"Auto Spread": "Otomatik Yayılma",
"Fit Page": "Sayfaya Sığdır",
"Fit Width": "Genişliğe Sığdır"
"Fit Width": "Genişliğe Sığdır",
"Failed to select directory": "Dizini seçme başarısız oldu",
"The new data directory must be different from the current one.": "Yeni veri dizini mevcut olanla farklı olmalıdır.",
"Migration failed: {{error}}": "Taşıma işlemi başarısız oldu: {{error}}",
"Change Data Location": "Veri Konumunu Değiştir",
"Current Data Location": "Mevcut Veri Konumu",
"Total size: {{size}}": "Toplam boyut: {{size}}",
"Calculating file info...": "Dosya bilgileri hesaplanıyor...",
"New Data Location": "Yeni Veri Konumu",
"Choose Different Folder": "Farklı Klasör Seç",
"Choose New Folder": "Yeni Klasör Seç",
"Migrating data...": "Veri taşınıyor...",
"Copying: {{file}}": "Kopyalanıyor: {{file}}",
"{{current}} of {{total}} files": "{{current}} / {{total}} dosya",
"Migration completed successfully!": "Taşıma işlemi başarıyla tamamlandı!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Verileriniz yeni konuma taşındı. Lütfen işlemi tamamlamak için uygulamayı yeniden başlatın.",
"Migration failed": "Taşıma işlemi başarısız oldu",
"Important Notice": "Önemli Duyuru",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Bu, tüm uygulama verilerinizi yeni konuma taşıyacaktır. Hedefin yeterli boş alana sahip olduğundan emin olun.",
"Restart App": "Uygulamayı Yeniden Başlat",
"Start Migration": "Taşımayı Başlat",
"Advanced Settings": "Gelişmiş Ayarlar",
"File count: {{size}}": "Dosya sayısı: {{size}}"
}
@@ -582,5 +582,27 @@
"Single Page": "Одна сторінка",
"Auto Spread": "Автоматичне розповсюдження",
"Fit Page": "Підганяти під сторінку",
"Fit Width": "Підганяти під ширину"
"Fit Width": "Підганяти під ширину",
"Failed to select directory": "Не вдалося вибрати каталог",
"The new data directory must be different from the current one.": "Новий каталог даних повинен відрізнятися від поточного.",
"Migration failed: {{error}}": "Переміщення не вдалося: {{error}}",
"Change Data Location": "Змінити розташування даних",
"Current Data Location": "Поточне розташування даних",
"Total size: {{size}}": "Загальний розмір: {{size}}",
"Calculating file info...": "Обчислення інформації про файл...",
"New Data Location": "Нове розташування даних",
"Choose Different Folder": "Вибрати іншу папку",
"Choose New Folder": "Вибрати нову папку",
"Migrating data...": "Переміщення даних...",
"Copying: {{file}}": "Копіювання: {{file}}",
"{{current}} of {{total}} files": "{{current}} з {{total}} файлів",
"Migration completed successfully!": "Переміщення завершено успішно!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ваші дані були переміщені до нового розташування. Будь ласка, перезапустіть програму, щоб завершити процес.",
"Migration failed": "Переміщення не вдалося",
"Important Notice": "Важливе повідомлення",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Це перемістить всі ваші дані програми до нового розташування. Переконайтеся, що в призначенні достатньо вільного місця.",
"Restart App": "Перезапустити програму",
"Start Migration": "Почати переміщення",
"Advanced Settings": "Розширені налаштування",
"File count: {{size}}": "Кількість файлів: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "Một trang",
"Auto Spread": "Tự động trải rộng",
"Fit Page": "Vừa trang",
"Fit Width": "Vừa chiều rộng"
"Fit Width": "Vừa chiều rộng",
"Failed to select directory": "Không thể chọn thư mục",
"The new data directory must be different from the current one.": "Thư mục dữ liệu mới phải khác với thư mục hiện tại.",
"Migration failed: {{error}}": "Di chuyển không thành công: {{error}}",
"Change Data Location": "Thay đổi vị trí dữ liệu",
"Current Data Location": "Vị trí dữ liệu hiện tại",
"Total size: {{size}}": "Tổng kích thước: {{size}}",
"Calculating file info...": "Đang tính toán thông tin tệp...",
"New Data Location": "Vị trí dữ liệu mới",
"Choose Different Folder": "Chọn thư mục khác",
"Choose New Folder": "Chọn thư mục mới",
"Migrating data...": "Đang di chuyển dữ liệu...",
"Copying: {{file}}": "Đang sao chép: {{file}}",
"{{current}} of {{total}} files": "{{current}} của {{total}} tệp",
"Migration completed successfully!": "Di chuyển hoàn tất thành công!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Dữ liệu của bạn đã được chuyển đến vị trí mới. Vui lòng khởi động lại ứng dụng để hoàn tất quá trình.",
"Migration failed": "Di chuyển không thành công",
"Important Notice": "Thông báo quan trọng",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Điều này sẽ di chuyển tất cả dữ liệu ứng dụng của bạn đến vị trí mới. Hãy chắc chắn rằng đích đến có đủ không gian trống.",
"Restart App": "Khởi động lại ứng dụng",
"Start Migration": "Bắt đầu di chuyển",
"Advanced Settings": "Cài đặt nâng cao",
"File count: {{size}}": "Số lượng tệp: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "单页",
"Auto Spread": "自动展开",
"Fit Page": "适应页面",
"Fit Width": "适应宽度"
"Fit Width": "适应宽度",
"Failed to select directory": "选择目录失败",
"The new data directory must be different from the current one.": "新的数据目录必须与当前目录不同。",
"Migration failed: {{error}}": "迁移失败: {{error}}",
"Change Data Location": "更改数据位置",
"Current Data Location": "当前数据位置",
"Total size: {{size}}": "总大小: {{size}}",
"Calculating file info...": "正在计算文件信息...",
"New Data Location": "新数据位置",
"Choose Different Folder": "选择不同的文件夹",
"Choose New Folder": "选择新文件夹",
"Migrating data...": "正在迁移数据...",
"Copying: {{file}}": "正在复制: {{file}}",
"{{current}} of {{total}} files": "{{current}} / {{total}} 个文件",
"Migration completed successfully!": "迁移成功完成!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "您的数据已移动到新位置。请重新启动应用程序以完成此过程。",
"Migration failed": "迁移失败",
"Important Notice": "重要通知",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "这将把您所有的应用数据移动到新位置。请确保目标位置有足够的可用空间。",
"Restart App": "重启应用",
"Start Migration": "开始迁移",
"Advanced Settings": "高级设置",
"File count: {{size}}": "文件数量: {{size}}"
}
@@ -570,5 +570,27 @@
"Single Page": "單頁",
"Auto Spread": "自動展開",
"Fit Page": "適應頁面",
"Fit Width": "適應寬度"
"Fit Width": "適應寬度",
"Failed to select directory": "選擇目錄失敗",
"The new data directory must be different from the current one.": "新的數據目錄必須與當前目錄不同。",
"Migration failed: {{error}}": "遷移失敗: {{error}}",
"Change Data Location": "更改數據位置",
"Current Data Location": "當前數據位置",
"Total size: {{size}}": "總大小: {{size}}",
"Calculating file info...": "正在計算文件信息...",
"New Data Location": "新的數據位置",
"Choose Different Folder": "選擇不同的文件夾",
"Choose New Folder": "選擇新文件夾",
"Migrating data...": "正在遷移數據...",
"Copying: {{file}}": "正在複製: {{file}}",
"{{current}} of {{total}} files": "{{current}} / {{total}} 個文件",
"Migration completed successfully!": "遷移成功完成!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "您的數據已移動到新位置。請重新啟動應用程序以完成此過程。",
"Migration failed": "遷移失敗",
"Important Notice": "重要通知",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "這將把您所有的應用數據移動到新位置。請確保目標位置有足夠的可用空間。",
"Restart App": "重新啟動應用",
"Start Migration": "開始遷移",
"Advanced Settings": "高級設置",
"File count: {{size}}": "文件數量: {{size}}"
}
@@ -61,6 +61,17 @@
}
]
},
{
"identifier": "fs:scope",
"allow": [
{
"path": "**/Readest"
},
{
"path": "**/Readest/**/*"
}
]
},
{
"identifier": "http:default",
"allow": [
@@ -30,6 +30,7 @@
"$APPDATA/**/*",
"$APPCACHE/**/*",
"$TEMP/**/*",
"**/Readest/**/*",
"/private/var/mobile/Containers/Data/Application/**/*"
],
"deny": []
@@ -290,7 +290,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
groupContextMenuHandler(item as BooksGroup);
}
}, 100),
[itemSelected],
[itemSelected, settings.localBooksDir],
);
const { pressing, handlers } = useLongPress(
@@ -0,0 +1,399 @@
import React, { useEffect, useState } from 'react';
import {
RiFolderOpenLine,
RiCheckboxCircleFill,
RiErrorWarningFill,
RiLoader2Line,
} from 'react-icons/ri';
import { join } from '@tauri-apps/api/path';
import { relaunch } from '@tauri-apps/plugin-process';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { DATA_SUBDIR } from '@/services/constants';
import { FileItem } from '@/types/system';
import { getDirPath } from '@/utils/path';
import { getOSPlatform } from '@/utils/misc';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import Dialog from '@/components/Dialog';
import { formatBytes } from '@/utils/book';
export const setMigrateDataDirDialogVisible = (visible: boolean) => {
const dialog = document.getElementById('migrate_data_dir_window');
if (dialog) {
const event = new CustomEvent('setDialogVisibility', {
detail: { visible },
});
dialog.dispatchEvent(event);
}
};
type MigrationStatus = 'idle' | 'selecting' | 'migrating' | 'completed' | 'error';
interface MigrationProgress {
current: number;
total: number;
currentFile?: string;
}
export const MigrateDataWindow = () => {
const _ = useTranslation();
const { appService, envConfig } = useEnv();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [isOpen, setIsOpen] = useState(false);
const [currentDataDir, setCurrentDataDir] = useState('');
const [newDataDir, setNewDataDir] = useState('');
const [migrationStatus, setMigrationStatus] = useState<MigrationStatus>('idle');
const [migrationProgress, setMigrationProgress] = useState<MigrationProgress>({
current: 0,
total: 0,
});
const [errorMessage, setErrorMessage] = useState('');
const [filesToMigrate, setFilesToMigrate] = useState<FileItem[]>([]);
const [currentDirFileCount, setCurrentDirFileCount] = useState('');
const [currentDirFileSize, setCurrentDirFileSize] = useState(0);
useEffect(() => {
const handleCustomEvent = (event: CustomEvent) => {
setIsOpen(event.detail.visible);
if (event.detail.visible) {
loadCurrentDataDir();
}
};
const el = document.getElementById('migrate_data_dir_window');
if (el) {
el.addEventListener('setDialogVisibility', handleCustomEvent as EventListener);
}
return () => {
if (el) {
el.removeEventListener('setDialogVisibility', handleCustomEvent as EventListener);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadCurrentDataDir = async () => {
try {
if (!appService) return;
const dataDir = await appService.resolveFilePath('', 'Data');
setCurrentDataDir(dataDir);
const files = await appService.readDirectory(dataDir, 'None');
setFilesToMigrate(files);
setCurrentDirFileCount(files.length.toLocaleString());
setCurrentDirFileSize(files.reduce((acc, file) => acc + file.size, 0));
} catch (error) {
console.error('Error loading current data directory:', error);
}
};
const handleSelectNewDir = async () => {
setMigrationStatus('selecting');
setErrorMessage('');
try {
const selectedDir = await appService?.selectDirectory?.('write');
if (selectedDir) {
const newDataDir = await join(selectedDir, DATA_SUBDIR);
await appService?.createDir(newDataDir, 'None', true);
setNewDataDir(newDataDir);
setMigrationStatus('idle');
} else {
setMigrationStatus('idle');
}
} catch (error) {
console.error('Error selecting directory:', error);
setErrorMessage(_('Failed to select directory'));
setMigrationStatus('error');
}
};
const handleStartMigration = async () => {
if (!appService || !currentDataDir || !newDataDir || !filesToMigrate.length) return;
setMigrationStatus('migrating');
setErrorMessage('');
setMigrationProgress({ current: 0, total: 0 });
try {
if (newDataDir === currentDataDir) {
throw new Error(_('The new data directory must be different from the current one.'));
}
// Copy all files to new location
for (let i = 0; i < filesToMigrate.length; i++) {
const file = filesToMigrate[i]!;
setMigrationProgress({
current: i + 1,
total: filesToMigrate.length,
currentFile: file.path,
});
const srcPath = await join(currentDataDir, file.path);
const destPath = await join(newDataDir, file.path);
await appService.copyFile(srcPath, destPath, 'None');
}
// Verify all files copied
const filesMigrated = await appService.readDirectory(newDataDir, 'None');
for (const file of filesToMigrate) {
if (!filesMigrated.find((f) => f.path === file.path && f.size === file.size)) {
throw new Error(`File ${file.path} failed to copy.`);
}
}
// Delete old data directory
await appService.deleteDir(currentDataDir, 'None', true);
// Update settings for new data directory
const customRootDir = getDirPath(newDataDir);
await appService.setCustomRootDir(customRootDir);
settings.customRootDir = customRootDir;
settings.localBooksDir = await appService.resolveFilePath('', 'Books');
setSettings({ ...settings });
await saveSettings(envConfig, settings);
// Finalize migration
setMigrationStatus('completed');
setCurrentDataDir(newDataDir);
setFilesToMigrate([]);
setCurrentDirFileCount('');
setCurrentDirFileSize(0);
loadCurrentDataDir();
} catch (error) {
console.error('Error migrating data:', error);
setErrorMessage(_('Migration failed: {{error}}', { error: error || 'Unknown error' }));
setMigrationStatus('error');
}
};
const handleClose = () => {
if (migrationStatus === 'migrating') {
return;
}
setIsOpen(false);
setNewDataDir('');
setMigrationStatus('idle');
setErrorMessage('');
setMigrationProgress({ current: 0, total: 0 });
};
const handleRestartApp = () => {
relaunch();
};
const handleRevealDir = (dataDir: string) => {
if (dataDir) {
revealItemInDir(dataDir);
}
};
const progressPercentage =
migrationProgress.total > 0
? Math.round((migrationProgress.current / migrationProgress.total) * 100)
: 0;
const canStartMigration =
newDataDir && newDataDir !== currentDataDir && migrationStatus === 'idle';
const osPlatform = getOSPlatform();
const fileRevealLabel =
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
return (
<Dialog
id='migrate_data_dir_window'
isOpen={isOpen}
title={_('Change Data Location')}
onClose={handleClose}
boxClassName='sm:!w-[520px] sm:!max-w-screen-sm sm:h-auto'
>
{isOpen && (
<div className='migrate-data-dir-content flex flex-col gap-6 px-6 py-4'>
{/* Current Data Directory */}
<div className='space-y-2'>
<h3 className='text-base-content text-sm font-semibold'>
{_('Current Data Location')}
</h3>
<button
title={_(fileRevealLabel)}
className='bg-base-200 flex w-full items-center gap-2 rounded-lg p-3'
onClick={() => handleRevealDir(currentDataDir)}
>
<RiFolderOpenLine className='text-base-content/70 h-4 w-4 flex-shrink-0' />
<span className='text-base-content/80 break-all text-start font-mono text-sm'>
{currentDataDir || _('Loading...')}
</span>
</button>
{currentDirFileCount ? (
<div className='flex space-x-4'>
<p className='text-base-content/60 text-xs'>
{_('File count: {{size}}', { size: currentDirFileCount })}
</p>
<p className='text-base-content/60 text-xs'>
{_('Total size: {{size}}', { size: formatBytes(currentDirFileSize) })}
</p>
</div>
) : (
<p className='text-base-content/60 text-xs'>{_('Calculating file info...')}</p>
)}
</div>
{/* New Data Directory Selection */}
<div className='space-y-3'>
<h3 className='text-base-content text-sm font-semibold'>{_('New Data Location')}</h3>
{newDataDir && (
<button
title={_(fileRevealLabel)}
className='bg-primary/10 border-primary/20 flex w-full items-center gap-2 rounded-lg border p-3'
onClick={() => handleRevealDir(newDataDir)}
>
<RiFolderOpenLine className='text-primary h-4 w-4 flex-shrink-0' />
<span className='text-primary break-all text-start font-mono text-sm'>
{newDataDir}
</span>
</button>
)}
<button
className='btn btn-outline btn-sm w-full'
onClick={handleSelectNewDir}
disabled={migrationStatus === 'migrating' || migrationStatus === 'selecting'}
>
{migrationStatus === 'selecting' && (
<RiLoader2Line className='h-4 w-4 animate-spin' />
)}
{newDataDir ? _('Choose Different Folder') : _('Choose New Folder')}
</button>
</div>
{/* Migration Progress */}
{migrationStatus === 'migrating' && (
<div className='space-y-3'>
<div className='flex items-center gap-2'>
<RiLoader2Line className='text-primary h-4 w-4 animate-spin' />
<span className='text-base-content text-sm font-medium'>
{_('Migrating data...')}
</span>
<span className='text-base-content/70 text-sm'>{progressPercentage}%</span>
</div>
<div className='bg-base-200 h-2 w-full rounded-full'>
<div
className='bg-primary h-2 rounded-full transition-all duration-300'
style={{ width: `${progressPercentage}%` }}
/>
</div>
{migrationProgress.currentFile && (
<p
className='text-base-content/60 overflow-hidden font-mono text-xs'
style={{
direction: 'rtl',
textAlign: 'left',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{_('Copying: {{file}}', { file: migrationProgress.currentFile })}
</p>
)}
<p className='text-base-content/60 text-xs'>
{_('{{current}} of {{total}} files', {
current: migrationProgress.current.toLocaleString(),
total: migrationProgress.total.toLocaleString(),
})}
</p>
</div>
)}
{/* Success State */}
{migrationStatus === 'completed' && (
<div className='space-y-3'>
<div className='text-success flex items-center gap-2'>
<RiCheckboxCircleFill className='h-5 w-5' />
<span className='font-medium'>{_('Migration completed successfully!')}</span>
</div>
<div className='bg-success/10 border-success/20 rounded-lg border p-3'>
<p className='text-success/80 text-sm'>
{_(
'Your data has been moved to the new location. Please restart the application to complete the process.',
)}
</p>
</div>
</div>
)}
{/* Error State */}
{migrationStatus === 'error' && errorMessage && (
<div className='space-y-2'>
<div className='text-error flex items-center gap-2'>
<RiErrorWarningFill className='h-5 w-5' />
<span className='font-medium'>{_('Migration failed')}</span>
</div>
<div className='bg-error/10 border-error/20 rounded-lg border p-3'>
<p className='text-error/80 text-sm'>{errorMessage}</p>
</div>
</div>
)}
{/* Warning */}
{canStartMigration && (
<div className='bg-warning/10 border-warning/20 rounded-lg border p-3'>
<div className='flex items-start gap-2'>
<RiErrorWarningFill className='text-warning mt-0.5 h-4 w-4 flex-shrink-0' />
<div className='space-y-1'>
<p className='text-base-content text-sm font-medium'>{_('Important Notice')}</p>
<p className='text-base-content/80 text-sm'>
{_(
'This will move all your app data to the new location. Make sure the destination has enough free space.',
)}
</p>
</div>
</div>
</div>
)}
{/* Action Buttons */}
<div className='flex gap-3 pt-2'>
{migrationStatus === 'completed' ? (
<>
<button className='btn btn-outline flex-1' onClick={handleClose}>
{_('Close')}
</button>
<button className='btn btn-primary flex-1' onClick={handleRestartApp}>
{_('Restart App')}
</button>
</>
) : (
<>
<button
className='btn btn-outline flex-1'
onClick={handleClose}
disabled={migrationStatus === 'migrating'}
>
{_('Cancel')}
</button>
<button
className='btn btn-primary flex-1'
onClick={handleStartMigration}
disabled={!canStartMigration || migrationStatus !== 'idle'}
>
{migrationStatus === 'migrating' && (
<RiLoader2Line className='h-4 w-4 animate-spin' />
)}
{_('Start Migration')}
</button>
</>
)}
</div>
</div>
)}
</Dialog>
);
};
@@ -6,7 +6,6 @@ import { PiUserCircleCheck } from 'react-icons/pi';
import { TbSunMoon } from 'react-icons/tb';
import { BiMoon, BiSun } from 'react-icons/bi';
import { setAboutDialogVisible } from '@/components/AboutWindow';
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
import { DOWNLOAD_READEST_URL } from '@/services/constants';
import { useAuth } from '@/context/AuthContext';
@@ -19,6 +18,8 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
import { setAboutDialogVisible } from '@/components/AboutWindow';
import { setMigrateDataDirDialogVisible } from '@/app/library/components/MigrateDataWindow';
import UserAvatar from '@/components/UserAvatar';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
@@ -162,6 +163,11 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
const handleSetRootDir = () => {
setMigrateDataDirDialogVisible(true);
setIsDropdownOpen?.(false);
};
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
const userFullName = user?.user_metadata?.['full_name'];
const userDisplayName = userFullName ? userFullName.split(' ')[0] : null;
@@ -262,6 +268,16 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon}
onClick={cycleThemeMode}
/>
{appService?.isDesktopApp && !appService?.isPortableApp && (
<>
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem label={_('Advanced Settings')}>
<ul className='flex flex-col'>
<MenuItem label={_('Change Data Location')} noIcon onClick={handleSetRootDir} />
</ul>
</MenuItem>
</>
)}
<hr aria-hidden='true' className='border-base-200 my-1' />
{user && userPlan === 'free' && !appService?.isIOSApp && (
<MenuItem label={_('Upgrade to Readest Premium')} onClick={handleUpgrade} />
+4 -2
View File
@@ -43,10 +43,11 @@ import {
tauriQuitApp,
} from '@/utils/window';
import { AboutWindow } from '@/components/AboutWindow';
import { UpdaterWindow } from '@/components/UpdaterWindow';
import { BookMetadata } from '@/libs/document';
import { AboutWindow } from '@/components/AboutWindow';
import { BookDetailModal } from '@/components/metadata';
import { UpdaterWindow } from '@/components/UpdaterWindow';
import { MigrateDataWindow } from './components/MigrateDataWindow';
import { Toast } from '@/components/Toast';
import Spinner from '@/components/Spinner';
import LibraryHeader from './components/LibraryHeader';
@@ -729,6 +730,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
)}
<AboutWindow />
<UpdaterWindow />
<MigrateDataWindow />
<Toast />
</div>
);
@@ -13,7 +13,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import {
formatAuthors,
formatDate,
formatFileSize,
formatBytes,
formatLanguage,
formatPublisher,
formatTitle,
@@ -158,9 +158,7 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('File Size')}</span>
<p className='text-neutral-content text-sm'>
{formatFileSize(fileSize) || _('Unknown')}
</p>
<p className='text-neutral-content text-sm'>{formatBytes(fileSize) || _('Unknown')}</p>
</div>
</div>
<div>
+34 -4
View File
@@ -1,6 +1,13 @@
import { v4 as uuidv4 } from 'uuid';
import { SystemSettings } from '@/types/settings';
import { AppPlatform, AppService, OsPlatform, ResolvedPath } from '@/types/system';
import {
AppPlatform,
AppService,
FileItem,
OsPlatform,
ResolvedPath,
SelectDirectoryMode,
} from '@/types/system';
import { FileSystem, BaseDir, DeleteAction } from '@/types/system';
import {
Book,
@@ -68,6 +75,7 @@ export abstract class BaseAppService implements AppService {
isIOSApp = false;
isMobileApp = false;
isPortableApp = false;
isDesktopApp = false;
hasTrafficLight = false;
hasWindow = false;
hasWindowBar = false;
@@ -83,10 +91,12 @@ export abstract class BaseAppService implements AppService {
protected abstract fs: FileSystem;
protected abstract resolvePath(fp: string, base: BaseDir): ResolvedPath;
abstract selectDirectory(): Promise<string>;
abstract init(): Promise<void>;
abstract setCustomRootDir(customRootDir: string): Promise<void>;
abstract selectDirectory(mode: SelectDirectoryMode): Promise<string>;
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
async init() {
async prepareBooksDir() {
this.localBooksDir = await this.fs.getPrefix('Books');
}
@@ -94,9 +104,29 @@ export abstract class BaseAppService implements AppService {
return await this.fs.openFile(path, base);
}
async copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise<void> {
return await this.fs.copyFile(srcPath, dstPath, base);
}
async createDir(path: string, base: BaseDir, recursive: boolean = true): Promise<void> {
return await this.fs.createDir(path, base, recursive);
}
async deleteFile(path: string, base: BaseDir): Promise<void> {
return await this.fs.removeFile(path, base);
}
async deleteDir(path: string, base: BaseDir, recursive: boolean = true): Promise<void> {
return await this.fs.removeDir(path, base, recursive);
}
async resolveFilePath(path: string, base: BaseDir): Promise<string> {
const prefix = await this.fs.getPrefix(base);
return `${prefix}/${path}`;
return path ? `${prefix}/${path}` : prefix;
}
async readDirectory(path: string, base: BaseDir): Promise<FileItem[]> {
return await this.fs.readDir(path, base);
}
getCoverImageUrl = (book: Book): string => {
+4 -4
View File
@@ -15,10 +15,10 @@ import { UserStorageQuota, UserDailyTranslationQuota } from '@/types/user';
import { getDefaultMaxBlockSize, getDefaultMaxInlineSize } from '@/utils/config';
import { stubTranslation as _ } from '@/utils/misc';
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
export const LOCAL_FONTS_SUBDIR = 'Readest/Fonts';
export const LOCAL_DATA_SUBDIR = 'Readest/Data';
export const DATA_SUBDIR = 'Readest';
export const LOCAL_BOOKS_SUBDIR = `${DATA_SUBDIR}/Books`;
export const CLOUD_BOOKS_SUBDIR = `${DATA_SUBDIR}/Books`;
export const LOCAL_FONTS_SUBDIR = `${DATA_SUBDIR}/Fonts`;
export const SETTINGS_FILENAME = 'settings.json';
@@ -8,8 +8,10 @@ import {
readDir,
remove,
copyFile,
stat,
BaseDirectory,
WriteFileOptions,
DirEntry,
} from '@tauri-apps/plugin-fs';
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
@@ -24,7 +26,7 @@ import {
} from '@tauri-apps/api/path';
import { type as osType } from '@tauri-apps/plugin-os';
import { FileSystem, BaseDir, AppPlatform, ResolvedPath } from '@/types/system';
import { FileSystem, BaseDir, AppPlatform, ResolvedPath, FileItem } from '@/types/system';
import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
import { getDirPath, getFilename } from '@/utils/path';
import { NativeFile, RemoteFile } from '@/utils/file';
@@ -32,8 +34,8 @@ import { copyURIToPath } from '@/utils/bridge';
import { BaseAppService } from './appService';
import {
DATA_SUBDIR,
LOCAL_BOOKS_SUBDIR,
LOCAL_DATA_SUBDIR,
LOCAL_FONTS_SUBDIR,
SETTINGS_FILENAME,
} from './constants';
@@ -67,8 +69,10 @@ const getPathResolver = ({
const isCustomBaseDir = Boolean(customRootDir);
const getCustomBasePrefixSync = isCustomBaseDir
? (baseDir: BaseDir) => {
return () =>
`${customRootDir}/${['Settings', 'Data', 'Books', 'Fonts'].includes(baseDir) ? '' : baseDir}`;
return () => {
const leafDir = ['Settings', 'Data', 'Books', 'Fonts'].includes(baseDir) ? '' : baseDir;
return leafDir ? `${customRootDir}/${leafDir}` : customRootDir!;
};
}
: undefined;
@@ -84,7 +88,7 @@ const getPathResolver = ({
return {
baseDir: isPortable ? 0 : BaseDirectory.AppConfig,
basePrefix: isPortable && execDir ? async () => execDir : appConfigDir,
fp: isPortable && execDir ? `${execDir}/${path}` : path,
fp: isPortable && execDir ? `${execDir}${path ? `/${path}` : ''}` : path,
base,
};
case 'Cache':
@@ -98,7 +102,7 @@ const getPathResolver = ({
return {
baseDir: isCustomBaseDir ? 0 : BaseDirectory.AppLog,
basePrefix: customBasePrefix ?? appLogDir,
fp: customBasePrefixSync ? `${customBasePrefixSync()}/${path}` : path,
fp: customBasePrefixSync ? `${customBasePrefixSync()}${path ? `/${path}` : ''}` : path,
base,
};
case 'Data':
@@ -106,8 +110,8 @@ const getPathResolver = ({
baseDir: customBaseDir ?? BaseDirectory.AppData,
basePrefix: customBasePrefix ?? appDataDir,
fp: customBasePrefixSync
? `${customBasePrefixSync()}/${LOCAL_DATA_SUBDIR}/${path}`
: `${LOCAL_DATA_SUBDIR}/${path}`,
? `${customBasePrefixSync()}/${DATA_SUBDIR}${path ? `/${path}` : ''}`
: `${DATA_SUBDIR}${path ? `/${path}` : ''}`,
base,
};
case 'Books':
@@ -115,8 +119,8 @@ const getPathResolver = ({
baseDir: customBaseDir ?? BaseDirectory.AppData,
basePrefix: customBasePrefix || appDataDir,
fp: customBasePrefixSync
? `${customBasePrefixSync()}/${LOCAL_BOOKS_SUBDIR}/${path}`
: `${LOCAL_BOOKS_SUBDIR}/${path}`,
? `${customBasePrefixSync()}/${LOCAL_BOOKS_SUBDIR}${path ? `/${path}` : ''}`
: `${LOCAL_BOOKS_SUBDIR}${path ? `/${path}` : ''}`,
base,
};
case 'Fonts':
@@ -124,8 +128,8 @@ const getPathResolver = ({
baseDir: customBaseDir ?? BaseDirectory.AppData,
basePrefix: customBasePrefix || appDataDir,
fp: customBasePrefixSync
? `${customBasePrefixSync()}/${LOCAL_FONTS_SUBDIR}/${path}`
: `${LOCAL_FONTS_SUBDIR}/${path}`,
? `${customBasePrefixSync()}/${LOCAL_FONTS_SUBDIR}${path ? `/${path}` : ''}`
: `${LOCAL_FONTS_SUBDIR}${path ? `/${path}` : ''}`,
base,
};
case 'None':
@@ -152,7 +156,8 @@ export const nativeFileSystem: FileSystem = {
async getPrefix(base: BaseDir) {
const { basePrefix, fp, baseDir } = this.resolvePath('', base);
const basePath = await basePrefix();
let basePath = await basePrefix();
basePath = basePath.replace(/\/+$/, '');
return fp ? (baseDir === 0 ? fp : await join(basePath, fp)) : basePath;
},
getURL(path: string) {
@@ -200,6 +205,9 @@ export const nativeFileSystem: FileSystem = {
}
},
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
if (!(await this.exists(getDirPath(dstPath), base))) {
await this.createDir(getDirPath(dstPath), base, true);
}
if (isContentURI(srcPath)) {
const prefix = await this.getPrefix(base);
if (!prefix) {
@@ -264,13 +272,37 @@ export const nativeFileSystem: FileSystem = {
async readDir(path: string, base: BaseDir) {
const { fp, baseDir } = this.resolvePath(path, base);
const list = await readDir(fp, baseDir ? { baseDir } : undefined);
return list.map((entity) => {
return {
path: entity.name,
isDir: entity.isDirectory,
};
});
const entries = await readDir(fp, baseDir ? { baseDir } : undefined);
const fileList: FileItem[] = [];
const readDirRecursively = async (
parent: string,
relative: string,
entries: DirEntry[],
fileList: FileItem[],
) => {
for (const entry of entries) {
if (entry.isDirectory) {
const dir = await join(parent, entry.name);
const relativeDir = relative ? await join(relative, entry.name) : entry.name;
await readDirRecursively(
dir,
relativeDir,
await readDir(dir, baseDir ? { baseDir } : undefined),
fileList,
);
} else {
const filePath = await join(parent, entry.name);
const relativePath = relative ? await join(relative, entry.name) : entry.name;
const fileInfo = await stat(filePath, baseDir ? { baseDir } : undefined);
fileList.push({
path: relativePath,
size: fileInfo.size,
});
}
}
};
await readDirRecursively(path, '', entries, fileList);
return fileList;
},
async exists(path: string, base: BaseDir) {
const { fp, baseDir } = this.resolvePath(path, base);
@@ -294,6 +326,7 @@ export class NativeAppService extends BaseAppService {
override isMacOSApp = OS_TYPE === 'macos';
override isLinuxApp = OS_TYPE === 'linux';
override isMobileApp = ['android', 'ios'].includes(OS_TYPE);
override isDesktopApp = ['macos', 'windows', 'linux'].includes(OS_TYPE);
override hasTrafficLight = OS_TYPE === 'macos';
override hasWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
override hasWindowBar = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
@@ -310,9 +343,11 @@ export class NativeAppService extends BaseAppService {
(OS_TYPE === 'ios' && getOSPlatform() === 'ios') || OS_TYPE === 'android';
override canCustomRootDir = OS_TYPE !== 'ios';
override distChannel = process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest';
private execDir?: string = undefined;
override async init() {
const execDir = await invoke<string>('get_executable_dir');
this.execDir = execDir;
if (
process.env['NEXT_PUBLIC_PORTABLE_APP'] ||
(await this.fs.exists(`${execDir}/${SETTINGS_FILENAME}`, 'None'))
@@ -332,13 +367,22 @@ export class NativeAppService extends BaseAppService {
execDir,
});
}
await super.init();
await this.prepareBooksDir();
}
override resolvePath(fp: string, base: BaseDir): ResolvedPath {
return this.fs.resolvePath(fp, base);
}
async setCustomRootDir(customRootDir: string) {
this.fs.resolvePath = getPathResolver({
customRootDir,
isPortable: this.isPortableApp,
execDir: this.execDir,
});
await this.prepareBooksDir();
}
async selectDirectory(): Promise<string> {
const selected = await openDialog({
directory: true,
+21 -7
View File
@@ -1,16 +1,16 @@
import { FileSystem, BaseDir, AppPlatform, ResolvedPath } from '@/types/system';
import { FileSystem, BaseDir, AppPlatform, ResolvedPath, FileItem } from '@/types/system';
import { getOSPlatform, isValidURL } from '@/utils/misc';
import { RemoteFile } from '@/utils/file';
import { isPWA } from './environment';
import { BaseAppService } from './appService';
import { LOCAL_BOOKS_SUBDIR, LOCAL_DATA_SUBDIR, LOCAL_FONTS_SUBDIR } from './constants';
import { DATA_SUBDIR, LOCAL_BOOKS_SUBDIR, LOCAL_FONTS_SUBDIR } from './constants';
const basePrefix = async () => '';
const resolvePath = (path: string, base: BaseDir): ResolvedPath => {
switch (base) {
case 'Data':
return { baseDir: 0, basePrefix, fp: `${LOCAL_DATA_SUBDIR}/${path}`, base };
return { baseDir: 0, basePrefix, fp: `${DATA_SUBDIR}/${path}`, base };
case 'Books':
return { baseDir: 0, basePrefix, fp: `${LOCAL_BOOKS_SUBDIR}/${path}`, base };
case 'Fonts':
@@ -168,17 +168,27 @@ const indexedDBFileSystem: FileSystem = {
const { fp } = this.resolvePath(path, base);
const db = await openIndexedDB();
return new Promise<{ path: string; isDir: boolean }[]>((resolve, reject) => {
return new Promise<FileItem[]>((resolve, reject) => {
const transaction = db.transaction('files', 'readonly');
const store = transaction.objectStore('files');
const request = store.getAll();
request.onsuccess = () => {
const files = request.result as { path: string }[];
const files = request.result as { path: string; content: string | ArrayBuffer | Blob }[];
resolve(
files
.filter((file) => file.path.startsWith(fp))
.map((file) => ({ path: file.path.slice(fp.length + 1), isDir: false })),
.map((file) => ({
path: file.path.slice(fp.length + 1),
size:
file.content instanceof Blob
? file.content.size
: typeof file.content === 'string'
? file.content.length
: file.content instanceof ArrayBuffer
? file.content.byteLength
: 0,
})),
);
};
@@ -208,13 +218,17 @@ export class WebAppService extends BaseAppService {
override async init() {
await this.loadSettings();
await super.init();
await this.prepareBooksDir();
}
override resolvePath(fp: string, base: BaseDir): ResolvedPath {
return this.fs.resolvePath(fp, base);
}
async setCustomRootDir() {
// No-op in web environment
}
async selectDirectory(): Promise<string> {
throw new Error('selectDirectory is not supported in browser');
}
+16 -2
View File
@@ -8,6 +8,7 @@ export type AppPlatform = 'web' | 'tauri';
export type OsPlatform = 'android' | 'ios' | 'macos' | 'windows' | 'linux' | 'unknown';
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Fonts' | 'Log' | 'Cache' | 'Temp' | 'None';
export type DeleteAction = 'cloud' | 'local' | 'both';
export type SelectDirectoryMode = 'read' | 'write';
export type ResolvedPath = {
baseDir: number;
@@ -16,6 +17,11 @@ export type ResolvedPath = {
base: BaseDir;
};
export type FileItem = {
path: string;
size: number;
};
export interface FileSystem {
resolvePath(path: string, base: BaseDir): ResolvedPath;
getURL(path: string): string;
@@ -25,7 +31,7 @@ export interface FileSystem {
readFile(path: string, base: BaseDir, mode: 'text' | 'binary'): Promise<string | ArrayBuffer>;
writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File): Promise<void>;
removeFile(path: string, base: BaseDir): Promise<void>;
readDir(path: string, base: BaseDir): Promise<{ path: string; isDir: boolean }[]>;
readDir(path: string, base: BaseDir): Promise<FileItem[]>;
createDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
removeDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
exists(path: string, base: BaseDir): Promise<boolean>;
@@ -52,14 +58,22 @@ export interface AppService {
isMacOSApp: boolean;
isLinuxApp: boolean;
isPortableApp: boolean;
isDesktopApp: boolean;
distChannel: string;
init(): Promise<void>;
openFile(path: string, base: BaseDir): Promise<File>;
copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise<void>;
createDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
deleteFile(path: string, base: BaseDir): Promise<void>;
deleteDir(path: string, base: BaseDir, recursive?: boolean): Promise<void>;
setCustomRootDir(customRootDir: string): Promise<void>;
resolveFilePath(path: string, base: BaseDir): Promise<string>;
getCachedImageUrl(pathOrUrl: string): Promise<string>;
selectDirectory(): Promise<string>;
selectDirectory(mode: SelectDirectoryMode): Promise<string>;
selectFiles(name: string, extensions: string[]): Promise<string[]>;
readDirectory(path: string, base: BaseDir): Promise<FileItem[]>;
getDefaultViewSettings(): ViewSettings;
loadSettings(): Promise<SystemSettings>;
+10 -8
View File
@@ -172,16 +172,18 @@ export const formatDate = (date: string | number | Date | null | undefined, isUT
}
};
export const formatFileSize = (size: number | null) => {
if (size === null) return '';
const formatter = new Intl.NumberFormat('en', {
export const formatBytes = (bytes?: number | null, locale = 'en-US') => {
if (!bytes) return '';
const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const value = bytes / Math.pow(1024, i);
const formatter = new Intl.NumberFormat(locale, {
style: 'unit',
unit: 'byte',
unitDisplay: 'narrow',
notation: 'compact',
compactDisplay: 'short',
unit: units[i],
unitDisplay: 'short',
maximumFractionDigits: 2,
});
return formatter.format(size);
return formatter.format(value);
};
export const getCurrentPage = (book: Book, progress: BookProgress) => {