General improvements (#149)
* Improved eraser hit detection and added horizontal scrolling to AnnotationDock * fix: lock scroll tracking during PDF rotation to prevent page jumping in vertical mode * Implemented a multi-tab reading system for the PDF viewer. * Redesigned the About dialog on homescreen. * improved bulk file import support * Implemented document caching and improved tab restoration in the PDF viewer. * Implemented native link detection and information extraction in `pdfium_bridge.cpp` and integrated it into `PdfPageComposable.kt`. This includes adding `getLinkInfoAtPoint` to `NativePdfiumBridge` to handle URI, GoTo, and RemoteGoTo/Launch actions, and updating the tap gesture logic to prioritize native link handling. * Fixed index bounds and layout padding calculation in PdfViewerScreen
This commit is contained in:
parent
381193d774
commit
c8f361376f
10 changed files with 1236 additions and 601 deletions
|
|
@ -39,7 +39,19 @@ typedef void* (*FPDFLink_GetAnnot_t)(void* link);
|
|||
typedef int (*FPDFAnnot_GetFlags_t)(void* annot);
|
||||
typedef int (*FPDFAnnot_SetFlags_t)(void* annot, int flags);
|
||||
typedef unsigned long (*FPDFAnnot_GetFormFieldName_t)(void* hFPDFTextPage, void* annot, void* buffer, unsigned long buflen);
|
||||
typedef void* (*FPDFLink_GetLinkAtPoint_t)(void* page, double x, double y);
|
||||
typedef unsigned long (*FPDFAction_GetURIPath_t)(void* document, void* action, void* buffer, unsigned long buflen);
|
||||
typedef void* (*FPDFLink_GetDest_t)(void* document, void* link);
|
||||
typedef void* (*FPDFAction_GetDest_t)(void* document, void* action);
|
||||
typedef int (*FPDFDest_GetDestPageIndex_t)(void* document, void* dest);
|
||||
typedef unsigned long (*FPDFAction_GetFilePath_t)(void* action, void* buffer, unsigned long buflen);
|
||||
|
||||
static FPDFLink_GetLinkAtPoint_t get_link_at_point_func = nullptr;
|
||||
static FPDFAction_GetURIPath_t get_uri_path_func = nullptr;
|
||||
static FPDFLink_GetDest_t get_dest_func = nullptr;
|
||||
static FPDFAction_GetDest_t get_action_dest_func = nullptr;
|
||||
static FPDFDest_GetDestPageIndex_t get_dest_page_index_func = nullptr;
|
||||
static FPDFAction_GetFilePath_t get_file_path_func = nullptr;
|
||||
static std::mutex g_pdfium_mutex;
|
||||
static FPDFLink_GetAnnot_t get_link_annot_func = nullptr;
|
||||
static FPDFLink_GetAction_t get_link_action_func = nullptr;
|
||||
|
|
@ -124,6 +136,13 @@ static bool init_pdfium() {
|
|||
get_link_annot_func = (FPDFLink_GetAnnot_t) dlsym(pdfium_handle, "FPDFLink_GetAnnot");
|
||||
get_form_field_name_func = (FPDFAnnot_GetFormFieldName_t) dlsym(pdfium_handle, "FPDFAnnot_GetFormFieldName");
|
||||
|
||||
get_link_at_point_func = (FPDFLink_GetLinkAtPoint_t) dlsym(pdfium_handle, "FPDFLink_GetLinkAtPoint");
|
||||
get_uri_path_func = (FPDFAction_GetURIPath_t) dlsym(pdfium_handle, "FPDFAction_GetURIPath");
|
||||
get_dest_func = (FPDFLink_GetDest_t) dlsym(pdfium_handle, "FPDFLink_GetDest");
|
||||
get_action_dest_func = (FPDFAction_GetDest_t) dlsym(pdfium_handle, "FPDFAction_GetDest");
|
||||
get_dest_page_index_func = (FPDFDest_GetDestPageIndex_t) dlsym(pdfium_handle, "FPDFDest_GetDestPageIndex");
|
||||
get_file_path_func = (FPDFAction_GetFilePath_t) dlsym(pdfium_handle, "FPDFAction_GetFilePath");
|
||||
|
||||
// --- Validation & Logging ---
|
||||
bool success = get_annot_count_func && get_annot_func && get_annot_subtype_func &&
|
||||
get_annot_rect_func && get_annot_string_func;
|
||||
|
|
@ -442,7 +461,8 @@ Java_com_aryan_reader_pdf_NativePdfiumBridge_performClick(JNIEnv *env, jclass cl
|
|||
int count = get_annot_count_func(page);
|
||||
void* hitAnnot = nullptr;
|
||||
|
||||
// 1. Find which annotation was clicked
|
||||
LOGI("PdfLinkDiagnostic: [C++] performClick at x=%f, y=%f (Total annots: %d)", x, y, count);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
void* annot = get_annot_func(page, i);
|
||||
if (!annot) continue;
|
||||
|
|
@ -456,6 +476,12 @@ Java_com_aryan_reader_pdf_NativePdfiumBridge_performClick(JNIEnv *env, jclass cl
|
|||
|
||||
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
|
||||
hitAnnot = annot;
|
||||
int subtype = get_annot_subtype_func(hitAnnot);
|
||||
LOGI("PdfLinkDiagnostic: [C++] HIT! Annot Index %d, Subtype %d", i, subtype);
|
||||
if (get_annot_flags_func) {
|
||||
int flags = get_annot_flags_func(hitAnnot);
|
||||
LOGI("PdfLinkDiagnostic: [C++] Flags for hit annot: %d", flags);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -492,3 +518,91 @@ Java_com_aryan_reader_pdf_NativePdfiumBridge_performClick(JNIEnv *env, jclass cl
|
|||
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_aryan_reader_pdf_NativePdfiumBridge_getLinkInfoAtPoint(JNIEnv *env, jclass clazz, jlong docPtr, jlong pagePtr, jdouble x, jdouble y) {
|
||||
std::lock_guard<std::mutex> lock(g_pdfium_mutex);
|
||||
if (!init_pdfium()) {
|
||||
LOGE("PdfLinkDiagnostic: init_pdfium failed.");
|
||||
return nullptr;
|
||||
}
|
||||
if (!get_link_at_point_func) {
|
||||
LOGE("PdfLinkDiagnostic: get_link_at_point_func is null.");
|
||||
return nullptr;
|
||||
}
|
||||
if (pagePtr == 0 || docPtr == 0) {
|
||||
LOGE("PdfLinkDiagnostic: Missing Pointers -> pagePtr=%ld, docPtr=%ld", (long)pagePtr, (long)docPtr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void* page = reinterpret_cast<void*>(pagePtr);
|
||||
void* wrapperDoc = reinterpret_cast<void*>(docPtr);
|
||||
|
||||
void* doc = wrapperDoc ? *(void**)wrapperDoc : nullptr;
|
||||
|
||||
if (!doc) {
|
||||
LOGE("PdfLinkDiagnostic: Dereferenced doc pointer is null!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LOGI("PdfLinkDiagnostic: Checking native link at x=%f, y=%f", x, y);
|
||||
|
||||
void* link = get_link_at_point_func(page, x, y);
|
||||
if (!link) {
|
||||
LOGI("PdfLinkDiagnostic: No FPDF_LINK found at point.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LOGI("PdfLinkDiagnostic: FPDF_LINK found!");
|
||||
|
||||
if (get_link_action_func && get_action_type_func) {
|
||||
void* action = get_link_action_func(link);
|
||||
if (action) {
|
||||
unsigned long type = get_action_type_func(action);
|
||||
LOGI("PdfLinkDiagnostic: Action Type = %lu", type);
|
||||
|
||||
if (type == 3 && get_uri_path_func) { // 3 = URI
|
||||
unsigned long len = get_uri_path_func(doc, action, nullptr, 0);
|
||||
if (len > 0) {
|
||||
std::vector<char> buffer(len);
|
||||
get_uri_path_func(doc, action, buffer.data(), len);
|
||||
std::string uri(buffer.data());
|
||||
LOGI("PdfLinkDiagnostic: Extracted Action URI = %s", uri.c_str());
|
||||
std::string result = "URI:" + uri;
|
||||
return env->NewStringUTF(result.c_str());
|
||||
}
|
||||
} else if (type == 1 && get_action_dest_func && get_dest_page_index_func) { // 1 = GoTo
|
||||
void* dest = get_action_dest_func(doc, action);
|
||||
if (dest) {
|
||||
int pageIndex = get_dest_page_index_func(doc, dest);
|
||||
LOGI("PdfLinkDiagnostic: Extracted Action GoTo Page = %d", pageIndex);
|
||||
std::string result = "PAGE:" + std::to_string(pageIndex);
|
||||
return env->NewStringUTF(result.c_str());
|
||||
}
|
||||
} else if ((type == 2 || type == 4) && get_file_path_func) { // 2 = RemoteGoTo, 4 = Launch
|
||||
unsigned long len = get_file_path_func(action, nullptr, 0);
|
||||
if (len > 0) {
|
||||
std::vector<char> buffer(len);
|
||||
get_file_path_func(action, buffer.data(), len);
|
||||
std::string path(buffer.data());
|
||||
LOGI("PdfLinkDiagnostic: Extracted File Path = %s", path.c_str());
|
||||
std::string result = "URI:" + path;
|
||||
return env->NewStringUTF(result.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (get_dest_func && get_dest_page_index_func) {
|
||||
void* dest = get_dest_func(doc, link);
|
||||
if (dest) {
|
||||
int pageIndex = get_dest_page_index_func(doc, dest);
|
||||
LOGI("PdfLinkDiagnostic: Extracted Direct Dest Page = %d", pageIndex);
|
||||
std::string result = "PAGE:" + std::to_string(pageIndex);
|
||||
return env->NewStringUTF(result.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
LOGI("PdfLinkDiagnostic: Link found but payload was empty or unsupported.");
|
||||
return nullptr;
|
||||
}
|
||||
|
|
@ -221,9 +221,7 @@ fun HomeScreen(
|
|||
if (isContextualModeActive) {
|
||||
viewModel.clearContextualAction()
|
||||
}
|
||||
uris.forEach { uri ->
|
||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
||||
}
|
||||
viewModel.onFilesSelected(uris)
|
||||
}
|
||||
|
||||
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
|
||||
|
|
@ -232,9 +230,7 @@ fun HomeScreen(
|
|||
if (isContextualModeActive) {
|
||||
viewModel.clearContextualAction()
|
||||
}
|
||||
uris.forEach { uri ->
|
||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
||||
}
|
||||
viewModel.onFilesSelected(uris)
|
||||
}
|
||||
|
||||
val onSelectFileClick = {
|
||||
|
|
@ -311,7 +307,8 @@ fun HomeScreen(
|
|||
onShowDeviceManagement = viewModel::showDeviceManagementForDebug,
|
||||
onFolderSyncToggle = viewModel::setFolderSyncEnabled,
|
||||
onClearReflowCache = { showClearReflowCacheDialog = true },
|
||||
onRecentFilesLimitChange = viewModel::setRecentFilesLimit
|
||||
onRecentFilesLimitChange = viewModel::setRecentFilesLimit,
|
||||
onTabsToggle = viewModel::setTabsEnabled
|
||||
)
|
||||
} else {
|
||||
ContextualTopAppBar(
|
||||
|
|
@ -769,7 +766,8 @@ fun DefaultTopAppBar(
|
|||
onAboutClick: () -> Unit,
|
||||
onShowDeviceManagement: () -> Unit,
|
||||
onFolderSyncToggle: (Boolean) -> Unit,
|
||||
onRecentFilesLimitChange: (Int) -> Unit
|
||||
onRecentFilesLimitChange: (Int) -> Unit,
|
||||
onTabsToggle: (Boolean) -> Unit
|
||||
) {
|
||||
var showOptionsMenu by remember { mutableStateOf(false) }
|
||||
var showLimitMenu by remember { mutableStateOf(false) }
|
||||
|
|
@ -822,6 +820,17 @@ fun DefaultTopAppBar(
|
|||
showOptionsMenu = false
|
||||
})
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(text = { Text("Enable Multi-Tab Reading") }, onClick = {
|
||||
onTabsToggle(!uiState.isTabsEnabled)
|
||||
showOptionsMenu = false
|
||||
}, trailingIcon = {
|
||||
if (uiState.isTabsEnabled) {
|
||||
Icon(Icons.Default.Check, contentDescription = "Enabled")
|
||||
}
|
||||
})
|
||||
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_book_cache)) }, onClick = {
|
||||
onClearCache()
|
||||
|
|
|
|||
|
|
@ -199,9 +199,7 @@ fun LibraryScreen(
|
|||
if (isContextualModeActive) {
|
||||
viewModel.clearContextualAction()
|
||||
}
|
||||
uris.forEach { uri ->
|
||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
||||
}
|
||||
viewModel.onFilesSelected(uris)
|
||||
}
|
||||
|
||||
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
|
||||
|
|
@ -210,9 +208,7 @@ fun LibraryScreen(
|
|||
if (isContextualModeActive) {
|
||||
viewModel.clearContextualAction()
|
||||
}
|
||||
uris.forEach { uri ->
|
||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
||||
}
|
||||
viewModel.onFilesSelected(uris)
|
||||
}
|
||||
|
||||
val onSelectFileClick = {
|
||||
|
|
|
|||
|
|
@ -236,6 +236,10 @@ data class ReaderScreenState(
|
|||
val pinnedLibraryBookIds: Set<String> = emptySet(),
|
||||
val libraryFilters: LibraryFilters = LibraryFilters(),
|
||||
val recentFilesLimit: Int = 0,
|
||||
val isTabsEnabled: Boolean = false,
|
||||
val openTabIds: List<String> = emptyList(),
|
||||
val openTabs: List<RecentFileItem> = emptyList(),
|
||||
val activeTabBookId: String? = null,
|
||||
)
|
||||
|
||||
open class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
|
@ -335,7 +339,15 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
else null,
|
||||
pinnedHomeBookIds = prefs.getStringSet(KEY_PINNED_HOME, emptySet()) ?: emptySet(),
|
||||
pinnedLibraryBookIds = prefs.getStringSet(KEY_PINNED_LIBRARY, emptySet()) ?: emptySet(),
|
||||
recentFilesLimit = prefs.getInt(KEY_RECENT_FILES_LIMIT, 0)
|
||||
recentFilesLimit = prefs.getInt(KEY_RECENT_FILES_LIMIT, 0),
|
||||
isTabsEnabled = prefs.getBoolean(KEY_TABS_ENABLED, false),
|
||||
openTabIds = prefs.getString(KEY_OPEN_TAB_IDS, null)?.let {
|
||||
try {
|
||||
val arr = JSONArray(it)
|
||||
List(arr.length()) { i -> arr.getString(i) }
|
||||
} catch(_: Exception) { emptyList() }
|
||||
} ?: emptyList(),
|
||||
activeTabBookId = prefs.getString(KEY_ACTIVE_TAB, null),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -394,6 +406,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
if (internalState.recentFilesLimit > 0) combined.take(internalState.recentFilesLimit) else combined
|
||||
}
|
||||
|
||||
val allBaseFiles = recentFilesFromDb.filterNot { it.bookId.endsWith("_reflow") }
|
||||
val openTabsList = internalState.openTabIds.mapNotNull { tabId ->
|
||||
allBaseFiles.find { it.bookId == tabId }
|
||||
}
|
||||
|
||||
val validContextualItems = internalState.contextualActionItems.filter { contextItem ->
|
||||
baseVisibleFiles.any { dbItem -> dbItem.uriString == contextItem.uriString }
|
||||
}.toSet()
|
||||
|
|
@ -433,6 +450,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
rawLibraryFiles = baseVisibleFiles,
|
||||
contextualActionItems = validContextualItems,
|
||||
shelves = allShelves,
|
||||
openTabs = openTabsList,
|
||||
booksAvailableForAdding = booksAvailableForAdding
|
||||
)
|
||||
}.stateIn(
|
||||
|
|
@ -441,6 +459,103 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
initialValue = ReaderScreenState()
|
||||
)
|
||||
|
||||
fun setTabsEnabled(enabled: Boolean) {
|
||||
prefs.edit { putBoolean(KEY_TABS_ENABLED, enabled) }
|
||||
_internalState.update { it.copy(isTabsEnabled = enabled) }
|
||||
if (!enabled) {
|
||||
val active = _internalState.value.activeTabBookId
|
||||
val newTabs = if (active != null) listOf(active) else emptyList()
|
||||
prefs.edit { putString(KEY_OPEN_TAB_IDS, JSONArray(newTabs).toString()) }
|
||||
_internalState.update { it.copy(openTabIds = newTabs) }
|
||||
}
|
||||
}
|
||||
|
||||
fun switchTab(bookId: String) {
|
||||
Timber.tag("PdfTabSync").i("ViewModel: switchTab called for bookId: $bookId")
|
||||
val item = uiState.value.rawLibraryFiles.find { it.bookId == bookId } ?: run {
|
||||
Timber.tag("PdfTabSync").e("ViewModel: switchTab FAILED - BookId $bookId not found in library")
|
||||
return
|
||||
}
|
||||
|
||||
val currentTabs = _internalState.value.openTabIds.toMutableList()
|
||||
if (!currentTabs.contains(bookId)) {
|
||||
if (currentTabs.size >= 20) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
showBanner("Maximum of 20 tabs allowed. Please close a tab first.", isError = true)
|
||||
}
|
||||
return
|
||||
}
|
||||
currentTabs.add(bookId)
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
putString(KEY_ACTIVE_TAB, bookId)
|
||||
putString(KEY_OPEN_TAB_IDS, JSONArray(currentTabs).toString())
|
||||
}
|
||||
|
||||
val uri = item.getUri()
|
||||
Timber.tag("PdfTabSync").d("ViewModel: ActiveTab updated to $bookId. URI found: ${uri != null}")
|
||||
|
||||
uri?.let {
|
||||
Timber.tag("PdfTabSync").d("ViewModel: Setting new URI directly: $it")
|
||||
_internalState.update { state ->
|
||||
state.copy(
|
||||
openTabIds = currentTabs,
|
||||
activeTabBookId = bookId,
|
||||
selectedPdfUri = it,
|
||||
selectedBookId = bookId,
|
||||
selectedFileType = item.type,
|
||||
initialPageInBook = item.lastPage,
|
||||
initialBookmarksJson = item.bookmarksJson,
|
||||
isLoading = false,
|
||||
errorMessage = null
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
addFileToRecent(
|
||||
it,
|
||||
item.type,
|
||||
bookId,
|
||||
customDisplayName = item.displayName,
|
||||
isRecent = true,
|
||||
sourceFolderUri = item.sourceFolderUri
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
_internalState.update { it.copy(openTabIds = currentTabs, activeTabBookId = bookId) }
|
||||
}
|
||||
}
|
||||
|
||||
fun closeTab(bookId: String) {
|
||||
Timber.tag("PdfTabSync").i("ViewModel: closeTab called for $bookId")
|
||||
val currentTabs = _internalState.value.openTabIds.toMutableList()
|
||||
currentTabs.remove(bookId)
|
||||
|
||||
if (currentTabs.isEmpty()) {
|
||||
prefs.edit {
|
||||
remove(KEY_OPEN_TAB_IDS)
|
||||
remove(KEY_ACTIVE_TAB)
|
||||
}
|
||||
_internalState.update { it.copy(openTabIds = emptyList(), activeTabBookId = null) }
|
||||
clearSelectedFile()
|
||||
} else {
|
||||
val activeTab = _internalState.value.activeTabBookId
|
||||
if (activeTab == bookId) {
|
||||
val nextTabId = currentTabs.last()
|
||||
prefs.edit {
|
||||
putString(KEY_OPEN_TAB_IDS, JSONArray(currentTabs).toString())
|
||||
putString(KEY_ACTIVE_TAB, nextTabId)
|
||||
}
|
||||
_internalState.update { it.copy(openTabIds = currentTabs, activeTabBookId = nextTabId) }
|
||||
switchTab(nextTabId)
|
||||
} else {
|
||||
prefs.edit { putString(KEY_OPEN_TAB_IDS, JSONArray(currentTabs).toString()) }
|
||||
_internalState.update { it.copy(openTabIds = currentTabs) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSearchQueryChange(newQuery: String) {
|
||||
_internalState.update {
|
||||
if (it.isSearchActive) {
|
||||
|
|
@ -2643,6 +2758,67 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
return fileName ?: uri.lastPathSegment
|
||||
}
|
||||
|
||||
fun onFilesSelected(uris: List<Uri>) {
|
||||
if (uris.isEmpty()) return
|
||||
|
||||
if (uris.size == 1) {
|
||||
onFileSelected(uris.first(), isFromRecent = false)
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_internalState.update {
|
||||
it.copy(
|
||||
bannerMessage = BannerMessage(
|
||||
message = appContext.getString(R.string.banner_importing_multiple, uris.size),
|
||||
isPersistent = true
|
||||
),
|
||||
contextualActionItems = emptySet()
|
||||
)
|
||||
}
|
||||
|
||||
var importedCount = 0
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
for (externalUri in uris) {
|
||||
val importResult = prepareBookForImport(externalUri)
|
||||
if (importResult != null) {
|
||||
val (internalUri, bookId, type) = importResult
|
||||
val displayName = getFileNameFromUri(externalUri, appContext) ?: "Unknown File"
|
||||
|
||||
addFileToRecent(
|
||||
uri = internalUri,
|
||||
type = type,
|
||||
bookId = bookId,
|
||||
customDisplayName = displayName,
|
||||
isRecent = false,
|
||||
sourceFolderUri = null
|
||||
)
|
||||
importedCount++
|
||||
} else {
|
||||
val hash = FileHasher.calculateSha256 {
|
||||
appContext.contentResolver.openInputStream(externalUri)
|
||||
}
|
||||
if (hash != null && recentFilesRepository.getFileByBookId(hash) != null) {
|
||||
importedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_internalState.update {
|
||||
it.copy(
|
||||
bannerMessage = BannerMessage(
|
||||
message = "Imported $importedCount books. You can find them in the Library tab.",
|
||||
isPersistent = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Timber.tag("BulkImport").i("Bulk import complete. $importedCount files processed.")
|
||||
}
|
||||
}
|
||||
|
||||
fun onFileSelected(uri: Uri, isFromRecent: Boolean = false) {
|
||||
if (isFromRecent) {
|
||||
Timber.i("Opening recent file: $uri")
|
||||
|
|
@ -2904,12 +3080,30 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
}
|
||||
|
||||
private fun openBook(
|
||||
uri: Uri, bookId: String, type: FileType, originalDisplayName: String? = null
|
||||
uri: Uri, bookId: String, type: FileType, originalDisplayName: String? = null, suppressNavigation: Boolean = false
|
||||
) {
|
||||
val openBookStartTime = System.currentTimeMillis()
|
||||
Timber.tag("FileOpenPerf")
|
||||
.d("[$bookId] openBook START | type=$type | displayName=$originalDisplayName")
|
||||
|
||||
if (_internalState.value.isTabsEnabled && type == FileType.PDF) {
|
||||
val currentTabs = _internalState.value.openTabIds.toMutableList()
|
||||
if (!currentTabs.contains(bookId)) {
|
||||
if (currentTabs.size >= 20) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
showBanner("Maximum of 20 tabs allowed. Please close a tab first.", isError = true)
|
||||
}
|
||||
return
|
||||
}
|
||||
currentTabs.add(bookId)
|
||||
}
|
||||
prefs.edit {
|
||||
putString(KEY_OPEN_TAB_IDS, JSONArray(currentTabs).toString())
|
||||
putString(KEY_ACTIVE_TAB, bookId)
|
||||
}
|
||||
_internalState.update { it.copy(openTabIds = currentTabs, activeTabBookId = bookId) }
|
||||
}
|
||||
|
||||
if (uri.scheme != "opds-pse") {
|
||||
try {
|
||||
if (uri.scheme == "file") {
|
||||
|
|
@ -2980,6 +3174,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
isRecent = true,
|
||||
sourceFolderUri = null
|
||||
)
|
||||
|
||||
if (!suppressNavigation) {
|
||||
Timber.tag("FileSwitch").d("PDF state updated, emitting navigation event")
|
||||
_navigationEvent.send(NavigationEvent("pdf_viewer", bookId, uri))
|
||||
} else {
|
||||
Timber.tag("FileSwitch").d("PDF state updated, suppressing navigation event for smooth transition")
|
||||
}
|
||||
}
|
||||
} else if (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX) {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -3012,6 +3213,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
)
|
||||
}
|
||||
|
||||
if (!suppressNavigation) {
|
||||
Timber.tag("FileSwitch").d("EPUB state updated, emitting navigation event")
|
||||
_navigationEvent.send(NavigationEvent("epub_reader", bookId, uri))
|
||||
}
|
||||
|
||||
when (type) {
|
||||
FileType.EPUB -> {
|
||||
loadEpub(uri, bookId, customDisplayName = originalDisplayName)
|
||||
|
|
@ -4152,5 +4358,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
internal const val KEY_PINNED_HOME = "pinned_home_books"
|
||||
internal const val KEY_PINNED_LIBRARY = "pinned_library_books"
|
||||
private const val KEY_RECENT_FILES_LIMIT = "recent_files_limit"
|
||||
private const val KEY_TABS_ENABLED = "tabs_enabled"
|
||||
private const val KEY_OPEN_TAB_IDS = "open_tab_ids"
|
||||
private const val KEY_ACTIVE_TAB = "active_tab_book_id"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,15 @@ package com.aryan.reader.pdf
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -76,6 +77,7 @@ fun AnnotationDock(
|
|||
onToggleStylusOnlyMode: () -> Unit
|
||||
) {
|
||||
val showFullDock = isSticky || !isMinimized
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val dockHeight = 56.dp
|
||||
val buttonSize = 36.dp
|
||||
|
|
@ -93,7 +95,9 @@ fun AnnotationDock(
|
|||
modifier = modifier.height(dockHeight)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = horizontalPadding),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.horizontalScroll(scrollState),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||
) {
|
||||
|
|
@ -235,8 +239,6 @@ fun AnnotationDock(
|
|||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Undo
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ object NativePdfiumBridge {
|
|||
@JvmStatic external fun extractImagePixels(pagePtr: Long, index: Int, dimens: IntArray): IntArray?
|
||||
|
||||
@JvmStatic external fun performClick(pagePtr: Long, x: Double, y: Double): Boolean
|
||||
@JvmStatic external fun getLinkInfoAtPoint(docPtr: Long, pagePtr: Long, x: Double, y: Double): String?
|
||||
|
||||
@JvmStatic external fun getAnnotSubtypeAtPoint(pagePtr: Long, x: Double, y: Double): Int
|
||||
@JvmStatic external fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray?
|
||||
|
|
|
|||
|
|
@ -2427,14 +2427,14 @@ internal fun PdfPageComposable(
|
|||
val tapYInBitmap = tapInContentCoords.y
|
||||
|
||||
coroutineScope.launch {
|
||||
val wasHandled = withContext(Dispatchers.IO) {
|
||||
val nativeResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pdfDocumentItem.openPage(pdfPageIndex)?.use { page ->
|
||||
val pagePtr = page.getNativePointer()
|
||||
|
||||
if (pagePtr == 0L) {
|
||||
Timber.tag("PdfInteraction").e("Could not find native pointer for page $pdfPageIndex")
|
||||
return@withContext false
|
||||
return@withContext 0
|
||||
}
|
||||
|
||||
val pdfCoords = page.mapDeviceCoordsToPage(
|
||||
|
|
@ -2442,134 +2442,150 @@ internal fun PdfPageComposable(
|
|||
currentPageRotation, tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt()
|
||||
)
|
||||
|
||||
NativePdfiumBridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
|
||||
} ?: false
|
||||
val docPtr = try {
|
||||
val pdfDocKt = (pdfDocumentItem as? PdfDocumentWrapper)?.pdfDocument
|
||||
if (pdfDocKt != null) {
|
||||
val documentField = pdfDocKt.javaClass.getDeclaredField("document").apply { isAccessible = true }
|
||||
val docUInstance = documentField.get(pdfDocKt)
|
||||
if (docUInstance != null) {
|
||||
val ptrField = docUInstance.javaClass.getDeclaredField("mNativeDocPtr").apply { isAccessible = true }
|
||||
ptrField.get(docUInstance) as Long
|
||||
} else 0L
|
||||
} else 0L
|
||||
} catch (e: Exception) { 0L }
|
||||
|
||||
Timber.tag("PdfLinkDiagnostic").i("Extracted docPtr: $docPtr | pagePtr: $pagePtr")
|
||||
|
||||
val linkInfo = NativePdfiumBridge.getLinkInfoAtPoint(
|
||||
docPtr, pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble()
|
||||
)
|
||||
|
||||
if (linkInfo != null) {
|
||||
Timber.tag("PdfLinkDiagnostic").i(">>> Native Link Info Extracted: $linkInfo")
|
||||
if (linkInfo.startsWith("URI:")) {
|
||||
val url = linkInfo.substringAfter("URI:")
|
||||
withContext(Dispatchers.Main) { onLinkClicked(url) }
|
||||
return@withContext 1
|
||||
} else if (linkInfo.startsWith("PAGE:")) {
|
||||
val targetPage = linkInfo.substringAfter("PAGE:").toIntOrNull()
|
||||
if (targetPage != null && targetPage >= 0) {
|
||||
withContext(Dispatchers.Main) { onInternalLinkClicked(targetPage) }
|
||||
return@withContext 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val clickHandled = NativePdfiumBridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
|
||||
if (clickHandled) {
|
||||
return@withContext 2
|
||||
}
|
||||
return@withContext 0
|
||||
} ?: 0
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("PdfInteraction").e(e, "Interaction error")
|
||||
false
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
if (wasHandled) {
|
||||
if (nativeResult == 2) {
|
||||
Timber.tag("PdfInteraction").i("Action detected. Refreshing page.")
|
||||
tiles = emptyList()
|
||||
bitmapState = null
|
||||
isLoadingPage = true
|
||||
currentRenderedPageId = "ACTION_${System.currentTimeMillis()}"
|
||||
return@launch
|
||||
} else if (nativeResult == 1) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale
|
||||
val hitTolerance = with(density) { 16.dp.toPx() } / inputScale
|
||||
val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale
|
||||
val hitTolerance = with(density) { 16.dp.toPx() } / inputScale
|
||||
|
||||
Timber.d(
|
||||
"detectTapGestures: Tap at bitmap coords (${tapXInBitmap.toInt()}, ${tapYInBitmap.toInt()})"
|
||||
)
|
||||
Timber.d("detectTapGestures: Tap at bitmap coords (${tapXInBitmap.toInt()}, ${tapYInBitmap.toInt()})")
|
||||
|
||||
var tappedRect: Rect? = null
|
||||
val hitHighlightPair = userHighlightScreenRects.findLast { pair ->
|
||||
val hit = pair.second.find { r ->
|
||||
val hitLeft = r.left - hitTolerance
|
||||
val hitTop = r.top - hitTolerance
|
||||
val hitRight = r.right + hitTolerance
|
||||
val hitBottom = r.bottom + hitTolerance
|
||||
var tappedRect: Rect? = null
|
||||
val hitHighlightPair = userHighlightScreenRects.findLast { pair ->
|
||||
val hit = pair.second.find { r ->
|
||||
val hitLeft = r.left - hitTolerance
|
||||
val hitTop = r.top - hitTolerance
|
||||
val hitRight = r.right + hitTolerance
|
||||
val hitBottom = r.bottom + hitTolerance
|
||||
|
||||
tapXInBitmap in hitLeft..hitRight &&
|
||||
tapYInBitmap >= hitTop && tapYInBitmap <= hitBottom
|
||||
tapXInBitmap in hitLeft..hitRight && tapYInBitmap >= hitTop && tapYInBitmap <= hitBottom
|
||||
}
|
||||
if (hit != null) {
|
||||
tappedRect = hit
|
||||
true
|
||||
} else false
|
||||
}
|
||||
if (hit != null) {
|
||||
tappedRect = hit
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
val standardHit = standardAnnotScreenRects.findLast { (annot, screenRect) ->
|
||||
if (annot.subtype == 2) return@findLast false
|
||||
val standardHit = standardAnnotScreenRects.findLast { (annot, screenRect) ->
|
||||
if (annot.subtype == 2) return@findLast false
|
||||
|
||||
val left = min(screenRect.left, screenRect.right)
|
||||
val right = max(screenRect.left, screenRect.right)
|
||||
val top = min(screenRect.top, screenRect.bottom)
|
||||
val bottom = max(screenRect.top, screenRect.bottom)
|
||||
val left = min(screenRect.left, screenRect.right)
|
||||
val right = max(screenRect.left, screenRect.right)
|
||||
val top = min(screenRect.top, screenRect.bottom)
|
||||
val bottom = max(screenRect.top, screenRect.bottom)
|
||||
|
||||
val inflatedHitBox = Rect(
|
||||
(left - annotHitTolerance).toInt(),
|
||||
(top - annotHitTolerance).toInt(),
|
||||
(right + annotHitTolerance).toInt(),
|
||||
(bottom + annotHitTolerance).toInt()
|
||||
)
|
||||
val inflatedHitBox = Rect(
|
||||
(left - annotHitTolerance).toInt(),
|
||||
(top - annotHitTolerance).toInt(),
|
||||
(right + annotHitTolerance).toInt(),
|
||||
(bottom + annotHitTolerance).toInt()
|
||||
)
|
||||
|
||||
val isHit = inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
|
||||
|
||||
isHit
|
||||
}
|
||||
|
||||
if (standardHit != null) {
|
||||
val (annot, screenRect) = standardHit
|
||||
customMenuState = CustomPdfMenuState(
|
||||
selectedText = annot.contents ?: "No comment",
|
||||
anchorRect = screenRect,
|
||||
charRange = Pair(-1, -1),
|
||||
isComment = true,
|
||||
author = annot.author,
|
||||
annotation = annot
|
||||
)
|
||||
return@detectTapGestures
|
||||
}
|
||||
|
||||
if (hitHighlightPair != null && tappedRect != null) {
|
||||
val hitHighlight = hitHighlightPair.first
|
||||
|
||||
val combinedRect = Rect(hitHighlightPair.second.first())
|
||||
hitHighlightPair.second.forEach { combinedRect.union(it) }
|
||||
|
||||
customMenuState = CustomPdfMenuState(
|
||||
selectedText = hitHighlight.text,
|
||||
anchorRect = combinedRect,
|
||||
charRange = hitHighlight.range,
|
||||
isExistingHighlight = true,
|
||||
highlightId = hitHighlight.id
|
||||
)
|
||||
return@detectTapGestures
|
||||
}
|
||||
|
||||
Timber.d(
|
||||
"detectTapGestures: Tap at bitmap coords (${tapXInBitmap.toInt()}, ${tapYInBitmap.toInt()})"
|
||||
)
|
||||
|
||||
val clickedLink = pageLinks.firstOrNull { link ->
|
||||
link.tapBounds.contains(
|
||||
tapXInBitmap.toInt(), tapYInBitmap.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
if (clickedLink != null) {
|
||||
Timber.d(
|
||||
"PdfPageComposable: Link clicked. Ignoring selection logic."
|
||||
)
|
||||
if (clickedLink.destPageIdx != null && clickedLink.destPageIdx >= 0) {
|
||||
onInternalLinkClicked(clickedLink.destPageIdx)
|
||||
} else if (clickedLink.url != null) {
|
||||
onLinkClicked(clickedLink.url)
|
||||
inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
|
||||
}
|
||||
return@detectTapGestures
|
||||
}
|
||||
|
||||
val wasMenuVisible = customMenuState != null
|
||||
val wasSelectionVisible =
|
||||
selectionCharRange.value != null || ocrSelectionSymbolIndices != null
|
||||
if (standardHit != null) {
|
||||
val (annot, screenRect) = standardHit
|
||||
customMenuState = CustomPdfMenuState(
|
||||
selectedText = annot.contents ?: "No comment",
|
||||
anchorRect = screenRect,
|
||||
charRange = Pair(-1, -1),
|
||||
isComment = true,
|
||||
author = annot.author,
|
||||
annotation = annot
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
Timber.d(
|
||||
"PdfPageComposable: State check - MenuVisible=$wasMenuVisible, SelectionVisible=$wasSelectionVisible"
|
||||
)
|
||||
if (hitHighlightPair != null && tappedRect != null) {
|
||||
val hitHighlight = hitHighlightPair.first
|
||||
val combinedRect = Rect(hitHighlightPair.second.first())
|
||||
hitHighlightPair.second.forEach { combinedRect.union(it) }
|
||||
|
||||
if (wasMenuVisible || wasSelectionVisible) {
|
||||
Timber.d(
|
||||
"PdfPageComposable: Clearing selection/menu."
|
||||
)
|
||||
customMenuState = null
|
||||
selectionCharRange.value = null
|
||||
ocrSelectionSymbolIndices = null
|
||||
coroutineScope.launch {
|
||||
customMenuState = CustomPdfMenuState(
|
||||
selectedText = hitHighlight.text,
|
||||
anchorRect = combinedRect,
|
||||
charRange = hitHighlight.range,
|
||||
isExistingHighlight = true,
|
||||
highlightId = hitHighlight.id
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val clickedLink = pageLinks.firstOrNull { link ->
|
||||
link.tapBounds.contains(tapXInBitmap.toInt(), tapYInBitmap.toInt())
|
||||
}
|
||||
|
||||
if (clickedLink != null) {
|
||||
Timber.d("PdfPageComposable: Fallback pageLinks intercepted click.")
|
||||
if (clickedLink.destPageIdx != null && clickedLink.destPageIdx >= 0) {
|
||||
onInternalLinkClicked(clickedLink.destPageIdx)
|
||||
} else if (clickedLink.url != null) {
|
||||
onLinkClicked(clickedLink.url)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val wasMenuVisible = customMenuState != null
|
||||
val wasSelectionVisible = selectionCharRange.value != null || ocrSelectionSymbolIndices != null
|
||||
|
||||
if (wasMenuVisible || wasSelectionVisible) {
|
||||
customMenuState = null
|
||||
selectionCharRange.value = null
|
||||
ocrSelectionSymbolIndices = null
|
||||
updateSelectionVisuals(
|
||||
pdfDocumentItem,
|
||||
pdfPageIndex,
|
||||
|
|
@ -2578,12 +2594,9 @@ internal fun PdfPageComposable(
|
|||
actualBitmapHeightPx,
|
||||
currentPageRotation,
|
||||
)
|
||||
} else {
|
||||
currentOnSingleTap()
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"PdfPageComposable: No selection active. Calling onSingleTap()."
|
||||
)
|
||||
currentOnSingleTap()
|
||||
}
|
||||
}, onDoubleTap = { tapOffset ->
|
||||
if (isZoomEnabled && !isVerticalScroll) {
|
||||
|
|
|
|||
|
|
@ -333,20 +333,20 @@ internal fun PdfVerticalReader(
|
|||
val panXAnimatable = remember { Animatable(if ((screenWidth * fitZoom) < screenWidth) (screenWidth - (screenWidth * fitZoom)) / 2f else 0f) }
|
||||
val panYAnimatable = remember { Animatable(0f) }
|
||||
|
||||
var isResizing by remember { mutableStateOf(false) }
|
||||
var previousScreenWidth by remember { mutableFloatStateOf(0f) }
|
||||
LaunchedEffect(screenWidth, screenHeight) {
|
||||
if (previousScreenWidth > 0f && previousScreenWidth != screenWidth) {
|
||||
if (zoomAnimatable.value <= 1.1f) {
|
||||
val centeredX = if ((screenWidth * fitZoom) < screenWidth) {
|
||||
(screenWidth - (screenWidth * fitZoom)) / 2f
|
||||
} else 0f
|
||||
var previousScreenHeight by remember { mutableFloatStateOf(0f) }
|
||||
val targetPageDuringResize = remember { mutableIntStateOf(-1) }
|
||||
|
||||
zoomAnimatable.snapTo(fitZoom)
|
||||
panXAnimatable.snapTo(centeredX)
|
||||
onZoomChange(fitZoom)
|
||||
if (previousScreenWidth != screenWidth || previousScreenHeight != screenHeight) {
|
||||
if (previousScreenWidth > 0f) {
|
||||
isResizing = true
|
||||
if (targetPageDuringResize.intValue == -1) {
|
||||
targetPageDuringResize.intValue = state.currentPage
|
||||
}
|
||||
}
|
||||
previousScreenWidth = screenWidth
|
||||
previousScreenHeight = screenHeight
|
||||
}
|
||||
|
||||
var isInitialLayout by remember { mutableStateOf(true) }
|
||||
|
|
@ -354,20 +354,50 @@ internal fun PdfVerticalReader(
|
|||
|
||||
LaunchedEffect(layoutState.pages) {
|
||||
if (!isInitialLayout) {
|
||||
val targetPageIdx = state.currentPage
|
||||
val targetPageIdx = if (targetPageDuringResize.intValue != -1) {
|
||||
targetPageDuringResize.intValue
|
||||
} else {
|
||||
state.currentPage
|
||||
}
|
||||
|
||||
val newLayout = layoutState.pages
|
||||
val pageLayout = newLayout.getOrNull(targetPageIdx)
|
||||
|
||||
if (pageLayout != null) {
|
||||
val currentZoom = zoomAnimatable.value
|
||||
val targetPanY = headerHeightPx - (pageLayout.y * currentZoom)
|
||||
val zoomedDocHeight = layoutState.totalHeight * currentZoom
|
||||
val isFit = currentZoom <= 1.1f
|
||||
val targetZoom = if (isFit) fitZoom else currentZoom
|
||||
|
||||
val targetPanY = headerHeightPx - (pageLayout.y * targetZoom)
|
||||
val zoomedDocHeight = layoutState.totalHeight * targetZoom
|
||||
val minPanY = (screenHeight - footerHeightPx - zoomedDocHeight).coerceAtMost(headerHeightPx)
|
||||
val finalPanY = targetPanY.coerceIn(minPanY, headerHeightPx)
|
||||
|
||||
Timber.tag("PdfZoomDiagnostics").i("Layout changed (Orientation/Size). Snapping to Page $targetPageIdx at PanY: $finalPanY")
|
||||
panYAnimatable.snapTo(finalPanY)
|
||||
val targetPanX = if (isFit) {
|
||||
if ((screenWidth * targetZoom) < screenWidth) {
|
||||
(screenWidth - (screenWidth * targetZoom)) / 2f
|
||||
} else 0f
|
||||
} else {
|
||||
panXAnimatable.value
|
||||
}
|
||||
|
||||
panXAnimatable.updateBounds(null, null)
|
||||
panYAnimatable.updateBounds(null, null)
|
||||
|
||||
coroutineScope {
|
||||
launch { zoomAnimatable.snapTo(targetZoom) }
|
||||
launch { panXAnimatable.snapTo(targetPanX) }
|
||||
launch { panYAnimatable.snapTo(finalPanY) }
|
||||
}
|
||||
|
||||
panYAnimatable.updateBounds(lowerBound = minPanY, upperBound = headerHeightPx)
|
||||
state.currentPage = targetPageIdx
|
||||
if (isFit) onZoomChange(targetZoom)
|
||||
}
|
||||
|
||||
delay(50)
|
||||
isResizing = false
|
||||
targetPageDuringResize.intValue = -1
|
||||
}
|
||||
isInitialLayout = false
|
||||
}
|
||||
|
|
@ -410,9 +440,9 @@ internal fun PdfVerticalReader(
|
|||
var isDragging by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(
|
||||
totalDocHeight, screenHeight, headerHeightPx, footerHeightPx, zoomAnimatable.value, isInteracting, isFlinging
|
||||
totalDocHeight, screenHeight, headerHeightPx, footerHeightPx, zoomAnimatable.value, isInteracting, isFlinging, isResizing
|
||||
) {
|
||||
if (zoomAnimatable.isRunning || panXAnimatable.isRunning || panYAnimatable.isRunning || isInteracting || isFlinging) {
|
||||
if (zoomAnimatable.isRunning || panXAnimatable.isRunning || panYAnimatable.isRunning || isInteracting || isFlinging || isResizing) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
|
|
@ -641,9 +671,10 @@ internal fun PdfVerticalReader(
|
|||
selectedTool,
|
||||
zoomAnimatable.value,
|
||||
isInteracting,
|
||||
isFlinging
|
||||
isFlinging,
|
||||
isResizing
|
||||
) {
|
||||
if (isInteracting || isFlinging) return@LaunchedEffect
|
||||
if (isInteracting || isFlinging || isResizing) return@LaunchedEffect
|
||||
|
||||
val currentZoom = zoomAnimatable.value
|
||||
val zoomedDocHeight = totalDocHeight * currentZoom
|
||||
|
|
@ -1255,11 +1286,11 @@ internal fun PdfVerticalReader(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(visiblePages, screenHeight) {
|
||||
LaunchedEffect(visiblePages, screenHeight, isResizing) {
|
||||
snapshotFlow {
|
||||
Pair(panYAnimatable.value, zoomAnimatable.value)
|
||||
}.collectLatest { (panY, zoom) ->
|
||||
if (visiblePages.isNotEmpty()) {
|
||||
if (!isResizing && visiblePages.isNotEmpty()) {
|
||||
state.firstVisiblePage = visiblePages.first().index
|
||||
state.lastVisiblePage = visiblePages.last().index
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -625,4 +625,19 @@
|
|||
<string name="error_opening_dictionary">Error opening dictionary</string>
|
||||
<string name="error_opening_translate">Error opening translate app</string>
|
||||
<string name="error_opening_search">Error opening search app</string>
|
||||
|
||||
<!-- About Section -->
|
||||
<string name="about_app_name">Episteme</string>
|
||||
<string name="about_oss_version">Open Source Version</string>
|
||||
<string name="about_play_version">Playstore Version</string>
|
||||
<string name="about_version_name">Version %1$s</string>
|
||||
<string name="about_build_code">Build %1$s</string>
|
||||
<string name="about_github">GitHub</string>
|
||||
<string name="about_github_desc">Browse source code, star, fork, and report issues.</string>
|
||||
<string name="about_privacy">Privacy Policy</string>
|
||||
<string name="about_privacy_desc">How we handle your data.</string>
|
||||
<string name="about_terms">Terms of Service</string>
|
||||
<string name="about_terms_desc">Usage terms and conditions.</string>
|
||||
<string name="about_licenses_desc">Open source libraries used.</string>
|
||||
<string name="banner_importing_multiple">Importing %1$d books… They will appear in your Library shortly.</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue