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_GetFlags_t)(void* annot);
|
||||||
typedef int (*FPDFAnnot_SetFlags_t)(void* annot, int flags);
|
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 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 std::mutex g_pdfium_mutex;
|
||||||
static FPDFLink_GetAnnot_t get_link_annot_func = nullptr;
|
static FPDFLink_GetAnnot_t get_link_annot_func = nullptr;
|
||||||
static FPDFLink_GetAction_t get_link_action_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_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_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 ---
|
// --- Validation & Logging ---
|
||||||
bool success = get_annot_count_func && get_annot_func && get_annot_subtype_func &&
|
bool success = get_annot_count_func && get_annot_func && get_annot_subtype_func &&
|
||||||
get_annot_rect_func && get_annot_string_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);
|
int count = get_annot_count_func(page);
|
||||||
void* hitAnnot = nullptr;
|
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++) {
|
for (int i = 0; i < count; i++) {
|
||||||
void* annot = get_annot_func(page, i);
|
void* annot = get_annot_func(page, i);
|
||||||
if (!annot) continue;
|
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) {
|
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
|
||||||
hitAnnot = annot;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -492,3 +518,91 @@ Java_com_aryan_reader_pdf_NativePdfiumBridge_performClick(JNIEnv *env, jclass cl
|
||||||
|
|
||||||
return JNI_FALSE;
|
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) {
|
if (isContextualModeActive) {
|
||||||
viewModel.clearContextualAction()
|
viewModel.clearContextualAction()
|
||||||
}
|
}
|
||||||
uris.forEach { uri ->
|
viewModel.onFilesSelected(uris)
|
||||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
|
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
|
@ -232,9 +230,7 @@ fun HomeScreen(
|
||||||
if (isContextualModeActive) {
|
if (isContextualModeActive) {
|
||||||
viewModel.clearContextualAction()
|
viewModel.clearContextualAction()
|
||||||
}
|
}
|
||||||
uris.forEach { uri ->
|
viewModel.onFilesSelected(uris)
|
||||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val onSelectFileClick = {
|
val onSelectFileClick = {
|
||||||
|
|
@ -311,7 +307,8 @@ fun HomeScreen(
|
||||||
onShowDeviceManagement = viewModel::showDeviceManagementForDebug,
|
onShowDeviceManagement = viewModel::showDeviceManagementForDebug,
|
||||||
onFolderSyncToggle = viewModel::setFolderSyncEnabled,
|
onFolderSyncToggle = viewModel::setFolderSyncEnabled,
|
||||||
onClearReflowCache = { showClearReflowCacheDialog = true },
|
onClearReflowCache = { showClearReflowCacheDialog = true },
|
||||||
onRecentFilesLimitChange = viewModel::setRecentFilesLimit
|
onRecentFilesLimitChange = viewModel::setRecentFilesLimit,
|
||||||
|
onTabsToggle = viewModel::setTabsEnabled
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ContextualTopAppBar(
|
ContextualTopAppBar(
|
||||||
|
|
@ -769,7 +766,8 @@ fun DefaultTopAppBar(
|
||||||
onAboutClick: () -> Unit,
|
onAboutClick: () -> Unit,
|
||||||
onShowDeviceManagement: () -> Unit,
|
onShowDeviceManagement: () -> Unit,
|
||||||
onFolderSyncToggle: (Boolean) -> Unit,
|
onFolderSyncToggle: (Boolean) -> Unit,
|
||||||
onRecentFilesLimitChange: (Int) -> Unit
|
onRecentFilesLimitChange: (Int) -> Unit,
|
||||||
|
onTabsToggle: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
var showOptionsMenu by remember { mutableStateOf(false) }
|
var showOptionsMenu by remember { mutableStateOf(false) }
|
||||||
var showLimitMenu by remember { mutableStateOf(false) }
|
var showLimitMenu by remember { mutableStateOf(false) }
|
||||||
|
|
@ -822,6 +820,17 @@ fun DefaultTopAppBar(
|
||||||
showOptionsMenu = false
|
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()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_book_cache)) }, onClick = {
|
DropdownMenuItem(text = { Text(stringResource(R.string.options_clear_book_cache)) }, onClick = {
|
||||||
onClearCache()
|
onClearCache()
|
||||||
|
|
|
||||||
|
|
@ -199,9 +199,7 @@ fun LibraryScreen(
|
||||||
if (isContextualModeActive) {
|
if (isContextualModeActive) {
|
||||||
viewModel.clearContextualAction()
|
viewModel.clearContextualAction()
|
||||||
}
|
}
|
||||||
uris.forEach { uri ->
|
viewModel.onFilesSelected(uris)
|
||||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
|
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
|
@ -210,9 +208,7 @@ fun LibraryScreen(
|
||||||
if (isContextualModeActive) {
|
if (isContextualModeActive) {
|
||||||
viewModel.clearContextualAction()
|
viewModel.clearContextualAction()
|
||||||
}
|
}
|
||||||
uris.forEach { uri ->
|
viewModel.onFilesSelected(uris)
|
||||||
viewModel.onFileSelected(uri, isFromRecent = false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val onSelectFileClick = {
|
val onSelectFileClick = {
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,10 @@ data class ReaderScreenState(
|
||||||
val pinnedLibraryBookIds: Set<String> = emptySet(),
|
val pinnedLibraryBookIds: Set<String> = emptySet(),
|
||||||
val libraryFilters: LibraryFilters = LibraryFilters(),
|
val libraryFilters: LibraryFilters = LibraryFilters(),
|
||||||
val recentFilesLimit: Int = 0,
|
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) {
|
open class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
@ -335,7 +339,15 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
else null,
|
else null,
|
||||||
pinnedHomeBookIds = prefs.getStringSet(KEY_PINNED_HOME, emptySet()) ?: emptySet(),
|
pinnedHomeBookIds = prefs.getStringSet(KEY_PINNED_HOME, emptySet()) ?: emptySet(),
|
||||||
pinnedLibraryBookIds = prefs.getStringSet(KEY_PINNED_LIBRARY, 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
|
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 ->
|
val validContextualItems = internalState.contextualActionItems.filter { contextItem ->
|
||||||
baseVisibleFiles.any { dbItem -> dbItem.uriString == contextItem.uriString }
|
baseVisibleFiles.any { dbItem -> dbItem.uriString == contextItem.uriString }
|
||||||
}.toSet()
|
}.toSet()
|
||||||
|
|
@ -433,6 +450,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
rawLibraryFiles = baseVisibleFiles,
|
rawLibraryFiles = baseVisibleFiles,
|
||||||
contextualActionItems = validContextualItems,
|
contextualActionItems = validContextualItems,
|
||||||
shelves = allShelves,
|
shelves = allShelves,
|
||||||
|
openTabs = openTabsList,
|
||||||
booksAvailableForAdding = booksAvailableForAdding
|
booksAvailableForAdding = booksAvailableForAdding
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
|
|
@ -441,6 +459,103 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
initialValue = ReaderScreenState()
|
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) {
|
fun onSearchQueryChange(newQuery: String) {
|
||||||
_internalState.update {
|
_internalState.update {
|
||||||
if (it.isSearchActive) {
|
if (it.isSearchActive) {
|
||||||
|
|
@ -2643,6 +2758,67 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
return fileName ?: uri.lastPathSegment
|
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) {
|
fun onFileSelected(uri: Uri, isFromRecent: Boolean = false) {
|
||||||
if (isFromRecent) {
|
if (isFromRecent) {
|
||||||
Timber.i("Opening recent file: $uri")
|
Timber.i("Opening recent file: $uri")
|
||||||
|
|
@ -2904,12 +3080,30 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openBook(
|
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()
|
val openBookStartTime = System.currentTimeMillis()
|
||||||
Timber.tag("FileOpenPerf")
|
Timber.tag("FileOpenPerf")
|
||||||
.d("[$bookId] openBook START | type=$type | displayName=$originalDisplayName")
|
.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") {
|
if (uri.scheme != "opds-pse") {
|
||||||
try {
|
try {
|
||||||
if (uri.scheme == "file") {
|
if (uri.scheme == "file") {
|
||||||
|
|
@ -2980,6 +3174,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
isRecent = true,
|
isRecent = true,
|
||||||
sourceFolderUri = null
|
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) {
|
} 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 {
|
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) {
|
when (type) {
|
||||||
FileType.EPUB -> {
|
FileType.EPUB -> {
|
||||||
loadEpub(uri, bookId, customDisplayName = originalDisplayName)
|
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_HOME = "pinned_home_books"
|
||||||
internal const val KEY_PINNED_LIBRARY = "pinned_library_books"
|
internal const val KEY_PINNED_LIBRARY = "pinned_library_books"
|
||||||
private const val KEY_RECENT_FILES_LIMIT = "recent_files_limit"
|
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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
|
@ -76,6 +77,7 @@ fun AnnotationDock(
|
||||||
onToggleStylusOnlyMode: () -> Unit
|
onToggleStylusOnlyMode: () -> Unit
|
||||||
) {
|
) {
|
||||||
val showFullDock = isSticky || !isMinimized
|
val showFullDock = isSticky || !isMinimized
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
val dockHeight = 56.dp
|
val dockHeight = 56.dp
|
||||||
val buttonSize = 36.dp
|
val buttonSize = 36.dp
|
||||||
|
|
@ -93,7 +95,9 @@ fun AnnotationDock(
|
||||||
modifier = modifier.height(dockHeight)
|
modifier = modifier.height(dockHeight)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = horizontalPadding),
|
modifier = Modifier
|
||||||
|
.padding(horizontal = horizontalPadding)
|
||||||
|
.horizontalScroll(scrollState),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(spacing)
|
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||||
) {
|
) {
|
||||||
|
|
@ -235,8 +239,6 @@ fun AnnotationDock(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
// Undo
|
// Undo
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ object NativePdfiumBridge {
|
||||||
@JvmStatic external fun extractImagePixels(pagePtr: Long, index: Int, dimens: IntArray): IntArray?
|
@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 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 getAnnotSubtypeAtPoint(pagePtr: Long, x: Double, y: Double): Int
|
||||||
@JvmStatic external fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray?
|
@JvmStatic external fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray?
|
||||||
|
|
|
||||||
|
|
@ -2427,14 +2427,14 @@ internal fun PdfPageComposable(
|
||||||
val tapYInBitmap = tapInContentCoords.y
|
val tapYInBitmap = tapInContentCoords.y
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val wasHandled = withContext(Dispatchers.IO) {
|
val nativeResult = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
pdfDocumentItem.openPage(pdfPageIndex)?.use { page ->
|
pdfDocumentItem.openPage(pdfPageIndex)?.use { page ->
|
||||||
val pagePtr = page.getNativePointer()
|
val pagePtr = page.getNativePointer()
|
||||||
|
|
||||||
if (pagePtr == 0L) {
|
if (pagePtr == 0L) {
|
||||||
Timber.tag("PdfInteraction").e("Could not find native pointer for page $pdfPageIndex")
|
Timber.tag("PdfInteraction").e("Could not find native pointer for page $pdfPageIndex")
|
||||||
return@withContext false
|
return@withContext 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val pdfCoords = page.mapDeviceCoordsToPage(
|
val pdfCoords = page.mapDeviceCoordsToPage(
|
||||||
|
|
@ -2442,29 +2442,66 @@ internal fun PdfPageComposable(
|
||||||
currentPageRotation, tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt()
|
currentPageRotation, tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
NativePdfiumBridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
|
val docPtr = try {
|
||||||
} ?: false
|
val pdfDocKt = (pdfDocumentItem as? PdfDocumentWrapper)?.pdfDocument
|
||||||
} catch (e: Exception) {
|
if (pdfDocKt != null) {
|
||||||
Timber.tag("PdfInteraction").e(e, "Interaction error")
|
val documentField = pdfDocKt.javaClass.getDeclaredField("document").apply { isAccessible = true }
|
||||||
false
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasHandled) {
|
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")
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nativeResult == 2) {
|
||||||
Timber.tag("PdfInteraction").i("Action detected. Refreshing page.")
|
Timber.tag("PdfInteraction").i("Action detected. Refreshing page.")
|
||||||
tiles = emptyList()
|
tiles = emptyList()
|
||||||
bitmapState = null
|
bitmapState = null
|
||||||
isLoadingPage = true
|
isLoadingPage = true
|
||||||
currentRenderedPageId = "ACTION_${System.currentTimeMillis()}"
|
currentRenderedPageId = "ACTION_${System.currentTimeMillis()}"
|
||||||
}
|
return@launch
|
||||||
|
} else if (nativeResult == 1) {
|
||||||
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale
|
val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale
|
||||||
val hitTolerance = with(density) { 16.dp.toPx() } / inputScale
|
val hitTolerance = with(density) { 16.dp.toPx() } / inputScale
|
||||||
|
|
||||||
Timber.d(
|
Timber.d("detectTapGestures: Tap at bitmap coords (${tapXInBitmap.toInt()}, ${tapYInBitmap.toInt()})")
|
||||||
"detectTapGestures: Tap at bitmap coords (${tapXInBitmap.toInt()}, ${tapYInBitmap.toInt()})"
|
|
||||||
)
|
|
||||||
|
|
||||||
var tappedRect: Rect? = null
|
var tappedRect: Rect? = null
|
||||||
val hitHighlightPair = userHighlightScreenRects.findLast { pair ->
|
val hitHighlightPair = userHighlightScreenRects.findLast { pair ->
|
||||||
|
|
@ -2474,8 +2511,7 @@ internal fun PdfPageComposable(
|
||||||
val hitRight = r.right + hitTolerance
|
val hitRight = r.right + hitTolerance
|
||||||
val hitBottom = r.bottom + hitTolerance
|
val hitBottom = r.bottom + hitTolerance
|
||||||
|
|
||||||
tapXInBitmap in hitLeft..hitRight &&
|
tapXInBitmap in hitLeft..hitRight && tapYInBitmap >= hitTop && tapYInBitmap <= hitBottom
|
||||||
tapYInBitmap >= hitTop && tapYInBitmap <= hitBottom
|
|
||||||
}
|
}
|
||||||
if (hit != null) {
|
if (hit != null) {
|
||||||
tappedRect = hit
|
tappedRect = hit
|
||||||
|
|
@ -2498,9 +2534,7 @@ internal fun PdfPageComposable(
|
||||||
(bottom + annotHitTolerance).toInt()
|
(bottom + annotHitTolerance).toInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
val isHit = inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
|
inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
|
||||||
|
|
||||||
isHit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (standardHit != null) {
|
if (standardHit != null) {
|
||||||
|
|
@ -2513,12 +2547,11 @@ internal fun PdfPageComposable(
|
||||||
author = annot.author,
|
author = annot.author,
|
||||||
annotation = annot
|
annotation = annot
|
||||||
)
|
)
|
||||||
return@detectTapGestures
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hitHighlightPair != null && tappedRect != null) {
|
if (hitHighlightPair != null && tappedRect != null) {
|
||||||
val hitHighlight = hitHighlightPair.first
|
val hitHighlight = hitHighlightPair.first
|
||||||
|
|
||||||
val combinedRect = Rect(hitHighlightPair.second.first())
|
val combinedRect = Rect(hitHighlightPair.second.first())
|
||||||
hitHighlightPair.second.forEach { combinedRect.union(it) }
|
hitHighlightPair.second.forEach { combinedRect.union(it) }
|
||||||
|
|
||||||
|
|
@ -2529,47 +2562,30 @@ internal fun PdfPageComposable(
|
||||||
isExistingHighlight = true,
|
isExistingHighlight = true,
|
||||||
highlightId = hitHighlight.id
|
highlightId = hitHighlight.id
|
||||||
)
|
)
|
||||||
return@detectTapGestures
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d(
|
|
||||||
"detectTapGestures: Tap at bitmap coords (${tapXInBitmap.toInt()}, ${tapYInBitmap.toInt()})"
|
|
||||||
)
|
|
||||||
|
|
||||||
val clickedLink = pageLinks.firstOrNull { link ->
|
val clickedLink = pageLinks.firstOrNull { link ->
|
||||||
link.tapBounds.contains(
|
link.tapBounds.contains(tapXInBitmap.toInt(), tapYInBitmap.toInt())
|
||||||
tapXInBitmap.toInt(), tapYInBitmap.toInt()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clickedLink != null) {
|
if (clickedLink != null) {
|
||||||
Timber.d(
|
Timber.d("PdfPageComposable: Fallback pageLinks intercepted click.")
|
||||||
"PdfPageComposable: Link clicked. Ignoring selection logic."
|
|
||||||
)
|
|
||||||
if (clickedLink.destPageIdx != null && clickedLink.destPageIdx >= 0) {
|
if (clickedLink.destPageIdx != null && clickedLink.destPageIdx >= 0) {
|
||||||
onInternalLinkClicked(clickedLink.destPageIdx)
|
onInternalLinkClicked(clickedLink.destPageIdx)
|
||||||
} else if (clickedLink.url != null) {
|
} else if (clickedLink.url != null) {
|
||||||
onLinkClicked(clickedLink.url)
|
onLinkClicked(clickedLink.url)
|
||||||
}
|
}
|
||||||
return@detectTapGestures
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val wasMenuVisible = customMenuState != null
|
val wasMenuVisible = customMenuState != null
|
||||||
val wasSelectionVisible =
|
val wasSelectionVisible = selectionCharRange.value != null || ocrSelectionSymbolIndices != null
|
||||||
selectionCharRange.value != null || ocrSelectionSymbolIndices != null
|
|
||||||
|
|
||||||
Timber.d(
|
|
||||||
"PdfPageComposable: State check - MenuVisible=$wasMenuVisible, SelectionVisible=$wasSelectionVisible"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (wasMenuVisible || wasSelectionVisible) {
|
if (wasMenuVisible || wasSelectionVisible) {
|
||||||
Timber.d(
|
|
||||||
"PdfPageComposable: Clearing selection/menu."
|
|
||||||
)
|
|
||||||
customMenuState = null
|
customMenuState = null
|
||||||
selectionCharRange.value = null
|
selectionCharRange.value = null
|
||||||
ocrSelectionSymbolIndices = null
|
ocrSelectionSymbolIndices = null
|
||||||
coroutineScope.launch {
|
|
||||||
updateSelectionVisuals(
|
updateSelectionVisuals(
|
||||||
pdfDocumentItem,
|
pdfDocumentItem,
|
||||||
pdfPageIndex,
|
pdfPageIndex,
|
||||||
|
|
@ -2578,13 +2594,10 @@ internal fun PdfPageComposable(
|
||||||
actualBitmapHeightPx,
|
actualBitmapHeightPx,
|
||||||
currentPageRotation,
|
currentPageRotation,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d(
|
|
||||||
"PdfPageComposable: No selection active. Calling onSingleTap()."
|
|
||||||
)
|
|
||||||
currentOnSingleTap()
|
currentOnSingleTap()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, onDoubleTap = { tapOffset ->
|
}, onDoubleTap = { tapOffset ->
|
||||||
if (isZoomEnabled && !isVerticalScroll) {
|
if (isZoomEnabled && !isVerticalScroll) {
|
||||||
if (actualBitmapWidthPx == 0) return@detectTapGestures
|
if (actualBitmapWidthPx == 0) return@detectTapGestures
|
||||||
|
|
|
||||||
|
|
@ -333,20 +333,20 @@ internal fun PdfVerticalReader(
|
||||||
val panXAnimatable = remember { Animatable(if ((screenWidth * fitZoom) < screenWidth) (screenWidth - (screenWidth * fitZoom)) / 2f else 0f) }
|
val panXAnimatable = remember { Animatable(if ((screenWidth * fitZoom) < screenWidth) (screenWidth - (screenWidth * fitZoom)) / 2f else 0f) }
|
||||||
val panYAnimatable = remember { Animatable(0f) }
|
val panYAnimatable = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
var isResizing by remember { mutableStateOf(false) }
|
||||||
var previousScreenWidth by remember { mutableFloatStateOf(0f) }
|
var previousScreenWidth by remember { mutableFloatStateOf(0f) }
|
||||||
LaunchedEffect(screenWidth, screenHeight) {
|
var previousScreenHeight by remember { mutableFloatStateOf(0f) }
|
||||||
if (previousScreenWidth > 0f && previousScreenWidth != screenWidth) {
|
val targetPageDuringResize = remember { mutableIntStateOf(-1) }
|
||||||
if (zoomAnimatable.value <= 1.1f) {
|
|
||||||
val centeredX = if ((screenWidth * fitZoom) < screenWidth) {
|
|
||||||
(screenWidth - (screenWidth * fitZoom)) / 2f
|
|
||||||
} else 0f
|
|
||||||
|
|
||||||
zoomAnimatable.snapTo(fitZoom)
|
if (previousScreenWidth != screenWidth || previousScreenHeight != screenHeight) {
|
||||||
panXAnimatable.snapTo(centeredX)
|
if (previousScreenWidth > 0f) {
|
||||||
onZoomChange(fitZoom)
|
isResizing = true
|
||||||
|
if (targetPageDuringResize.intValue == -1) {
|
||||||
|
targetPageDuringResize.intValue = state.currentPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousScreenWidth = screenWidth
|
previousScreenWidth = screenWidth
|
||||||
|
previousScreenHeight = screenHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
var isInitialLayout by remember { mutableStateOf(true) }
|
var isInitialLayout by remember { mutableStateOf(true) }
|
||||||
|
|
@ -354,20 +354,50 @@ internal fun PdfVerticalReader(
|
||||||
|
|
||||||
LaunchedEffect(layoutState.pages) {
|
LaunchedEffect(layoutState.pages) {
|
||||||
if (!isInitialLayout) {
|
if (!isInitialLayout) {
|
||||||
val targetPageIdx = state.currentPage
|
val targetPageIdx = if (targetPageDuringResize.intValue != -1) {
|
||||||
|
targetPageDuringResize.intValue
|
||||||
|
} else {
|
||||||
|
state.currentPage
|
||||||
|
}
|
||||||
|
|
||||||
val newLayout = layoutState.pages
|
val newLayout = layoutState.pages
|
||||||
val pageLayout = newLayout.getOrNull(targetPageIdx)
|
val pageLayout = newLayout.getOrNull(targetPageIdx)
|
||||||
|
|
||||||
if (pageLayout != null) {
|
if (pageLayout != null) {
|
||||||
val currentZoom = zoomAnimatable.value
|
val currentZoom = zoomAnimatable.value
|
||||||
val targetPanY = headerHeightPx - (pageLayout.y * currentZoom)
|
val isFit = currentZoom <= 1.1f
|
||||||
val zoomedDocHeight = layoutState.totalHeight * currentZoom
|
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 minPanY = (screenHeight - footerHeightPx - zoomedDocHeight).coerceAtMost(headerHeightPx)
|
||||||
val finalPanY = targetPanY.coerceIn(minPanY, headerHeightPx)
|
val finalPanY = targetPanY.coerceIn(minPanY, headerHeightPx)
|
||||||
|
|
||||||
Timber.tag("PdfZoomDiagnostics").i("Layout changed (Orientation/Size). Snapping to Page $targetPageIdx at PanY: $finalPanY")
|
val targetPanX = if (isFit) {
|
||||||
panYAnimatable.snapTo(finalPanY)
|
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
|
isInitialLayout = false
|
||||||
}
|
}
|
||||||
|
|
@ -410,9 +440,9 @@ internal fun PdfVerticalReader(
|
||||||
var isDragging by remember { mutableStateOf(false) }
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(
|
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
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -641,9 +671,10 @@ internal fun PdfVerticalReader(
|
||||||
selectedTool,
|
selectedTool,
|
||||||
zoomAnimatable.value,
|
zoomAnimatable.value,
|
||||||
isInteracting,
|
isInteracting,
|
||||||
isFlinging
|
isFlinging,
|
||||||
|
isResizing
|
||||||
) {
|
) {
|
||||||
if (isInteracting || isFlinging) return@LaunchedEffect
|
if (isInteracting || isFlinging || isResizing) return@LaunchedEffect
|
||||||
|
|
||||||
val currentZoom = zoomAnimatable.value
|
val currentZoom = zoomAnimatable.value
|
||||||
val zoomedDocHeight = totalDocHeight * currentZoom
|
val zoomedDocHeight = totalDocHeight * currentZoom
|
||||||
|
|
@ -1255,11 +1286,11 @@ internal fun PdfVerticalReader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(visiblePages, screenHeight) {
|
LaunchedEffect(visiblePages, screenHeight, isResizing) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
Pair(panYAnimatable.value, zoomAnimatable.value)
|
Pair(panYAnimatable.value, zoomAnimatable.value)
|
||||||
}.collectLatest { (panY, zoom) ->
|
}.collectLatest { (panY, zoom) ->
|
||||||
if (visiblePages.isNotEmpty()) {
|
if (!isResizing && visiblePages.isNotEmpty()) {
|
||||||
state.firstVisiblePage = visiblePages.first().index
|
state.firstVisiblePage = visiblePages.first().index
|
||||||
state.lastVisiblePage = visiblePages.last().index
|
state.lastVisiblePage = visiblePages.last().index
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import android.widget.Toast
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
|
@ -97,6 +98,7 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
@ -111,10 +113,12 @@ import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.ArrowDownward
|
import androidx.compose.material.icons.filled.ArrowDownward
|
||||||
import androidx.compose.material.icons.filled.ArrowUpward
|
import androidx.compose.material.icons.filled.ArrowUpward
|
||||||
import androidx.compose.material.icons.filled.Brush
|
import androidx.compose.material.icons.filled.Brush
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Fullscreen
|
import androidx.compose.material.icons.filled.Fullscreen
|
||||||
|
|
@ -1078,6 +1082,35 @@ private fun getFastFileId(context: Context, uri: Uri): String {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class DocumentCacheItem(
|
||||||
|
val doc: ReaderDocument,
|
||||||
|
val pfd: ParcelFileDescriptor,
|
||||||
|
val totalPages: Int,
|
||||||
|
val pageAspectRatios: List<Float>,
|
||||||
|
val flatTableOfContents: List<TocEntry>
|
||||||
|
)
|
||||||
|
|
||||||
|
private class DocumentCache(val maxSize: Int = 3) {
|
||||||
|
val cache = object : android.util.LruCache<String, DocumentCacheItem>(maxSize) {
|
||||||
|
override fun entryRemoved(
|
||||||
|
evicted: Boolean,
|
||||||
|
key: String,
|
||||||
|
oldValue: DocumentCacheItem,
|
||||||
|
newValue: DocumentCacheItem?
|
||||||
|
) {
|
||||||
|
if (evicted) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try { oldValue.doc.close() } catch (e: Exception) { Timber.e(e) }
|
||||||
|
try { oldValue.pfd.close() } catch (e: Exception) { Timber.e(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun put(key: String, item: DocumentCacheItem) { cache.put(key, item) }
|
||||||
|
fun get(key: String): DocumentCacheItem? = cache.get(key)
|
||||||
|
fun evictAll() { cache.evictAll() }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("KotlinConstantConditions")
|
@Suppress("KotlinConstantConditions")
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope", "ObsoleteSdkInt")
|
@SuppressLint("UnusedBoxWithConstraintsScope", "ObsoleteSdkInt")
|
||||||
@ExperimentalMaterial3Api
|
@ExperimentalMaterial3Api
|
||||||
|
|
@ -1110,6 +1143,8 @@ fun PdfViewerScreen(
|
||||||
var showThemePanel by remember { mutableStateOf(false) }
|
var showThemePanel by remember { mutableStateOf(false) }
|
||||||
var currentThemeId by remember { mutableStateOf(loadPdfThemeId(context)) }
|
var currentThemeId by remember { mutableStateOf(loadPdfThemeId(context)) }
|
||||||
var customThemes by remember { mutableStateOf(loadCustomThemes(context)) }
|
var customThemes by remember { mutableStateOf(loadCustomThemes(context)) }
|
||||||
|
val documentCache = remember { DocumentCache(3) }
|
||||||
|
val tabStateMap = remember { mutableStateMapOf<String, Int>() }
|
||||||
|
|
||||||
val activeTheme = remember(currentThemeId, customThemes) {
|
val activeTheme = remember(currentThemeId, customThemes) {
|
||||||
PdfBuiltInThemes.find { it.id == currentThemeId }
|
PdfBuiltInThemes.find { it.id == currentThemeId }
|
||||||
|
|
@ -1149,30 +1184,39 @@ fun PdfViewerScreen(
|
||||||
var isBackgroundIndexing by remember { mutableStateOf(false) }
|
var isBackgroundIndexing by remember { mutableStateOf(false) }
|
||||||
var backgroundIndexingProgress by remember { mutableFloatStateOf(0f) }
|
var backgroundIndexingProgress by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
var currentBookId by remember { mutableStateOf<String?>(null) }
|
|
||||||
val bookId = currentBookId ?: pdfUri.toString().hashCode().toString()
|
|
||||||
|
|
||||||
LaunchedEffect(bookId) {
|
|
||||||
isScrollLocked = loadPdfScrollLocked(context, bookId)
|
|
||||||
isFullScreen = loadPdfFullScreen(context, bookId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val effectivePdfUri = uiState.selectedPdfUri ?: pdfUri
|
||||||
|
val effectiveFileType = uiState.selectedFileType ?: FileType.PDF
|
||||||
|
|
||||||
|
var showNewTabSheet by remember { mutableStateOf(false) }
|
||||||
|
val sheetState = androidx.compose.material3.rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||||
|
|
||||||
|
val isTabsEnabled = uiState.isTabsEnabled
|
||||||
|
val openTabs = uiState.openTabs
|
||||||
|
val activeTabBookId = uiState.activeTabBookId
|
||||||
|
val originalFileName by remember(uiState.recentFiles, effectivePdfUri) {
|
||||||
|
derivedStateOf {
|
||||||
|
uiState.recentFiles.find { it.uriString == effectivePdfUri.toString() }?.displayName
|
||||||
|
?: effectivePdfUri.lastPathSegment ?: "Document.pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var currentBookId by remember { mutableStateOf<String?>(null) }
|
||||||
|
val bookId = currentBookId ?: effectivePdfUri.toString().hashCode().toString()
|
||||||
|
val view = LocalView.current
|
||||||
|
var isDockDragging by remember { mutableStateOf(false) }
|
||||||
|
var initialScrollDone by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val reflowBookId = remember(bookId) { "${bookId}_reflow" }
|
val reflowBookId = remember(bookId) { "${bookId}_reflow" }
|
||||||
val hasReflowFile by remember(uiState.allRecentFiles, reflowBookId) {
|
val hasReflowFile by remember(uiState.allRecentFiles, reflowBookId) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
uiState.allRecentFiles.any { it.bookId == reflowBookId && !it.isDeleted }
|
uiState.allRecentFiles.any { it.bookId == reflowBookId && !it.isDeleted }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val originalFileName by remember(uiState.recentFiles, pdfUri) {
|
|
||||||
derivedStateOf {
|
LaunchedEffect(bookId) {
|
||||||
uiState.recentFiles.find { it.uriString == pdfUri.toString() }?.displayName
|
isScrollLocked = loadPdfScrollLocked(context, bookId)
|
||||||
?: pdfUri.lastPathSegment ?: "Document.pdf"
|
isFullScreen = loadPdfFullScreen(context, bookId)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val view = LocalView.current
|
|
||||||
var isDockDragging by remember { mutableStateOf(false) }
|
|
||||||
var initialScrollDone by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var isAutoScrollModeActive by remember { mutableStateOf(false) }
|
var isAutoScrollModeActive by remember { mutableStateOf(false) }
|
||||||
var isAutoScrollPlaying by remember { mutableStateOf(false) }
|
var isAutoScrollPlaying by remember { mutableStateOf(false) }
|
||||||
|
|
@ -1254,7 +1298,7 @@ fun PdfViewerScreen(
|
||||||
Timber.tag("PdfPrint").d("Starting print job: $jobName")
|
Timber.tag("PdfPrint").d("Starting print job: $jobName")
|
||||||
printManager.print(
|
printManager.print(
|
||||||
jobName,
|
jobName,
|
||||||
PdfPrintDocumentAdapter(context, pdfUri, originalFileName),
|
PdfPrintDocumentAdapter(context, effectivePdfUri, originalFileName),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -1471,6 +1515,8 @@ fun PdfViewerScreen(
|
||||||
mutableStateMapOf<Int, MutableList<PdfAnnotation>>()
|
mutableStateMapOf<Int, MutableList<PdfAnnotation>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastEraserPoint by remember { mutableStateOf<PdfPoint?>(null) }
|
||||||
|
|
||||||
var areAnnotationsLoaded by remember { mutableStateOf(false) }
|
var areAnnotationsLoaded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val richTextRepository = remember(context) { PdfRichTextRepository(context) }
|
val richTextRepository = remember(context) { PdfRichTextRepository(context) }
|
||||||
|
|
@ -1513,6 +1559,7 @@ fun PdfViewerScreen(
|
||||||
if (initialScrollDone) {
|
if (initialScrollDone) {
|
||||||
Timber.tag("PdfPositionDebug").v("UI: Tracking | currentPage: $currentPage | pendingRestorePage updated")
|
Timber.tag("PdfPositionDebug").v("UI: Tracking | currentPage: $currentPage | pendingRestorePage updated")
|
||||||
pendingRestorePage = currentPage
|
pendingRestorePage = currentPage
|
||||||
|
currentBookId?.let { tabStateMap[it] = currentPage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1822,7 +1869,7 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
val onInsertPage: () -> Unit = {
|
val onInsertPage: () -> Unit = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val targetIndex = currentPage + 1
|
val targetIndex = (currentPage + 1).coerceIn(0, virtualPages.size)
|
||||||
Timber.tag("RichTextMigration").i("INSERT: User requested blank page at index $targetIndex")
|
Timber.tag("RichTextMigration").i("INSERT: User requested blank page at index $targetIndex")
|
||||||
|
|
||||||
val (refWidth, refHeight) = withContext(Dispatchers.IO) {
|
val (refWidth, refHeight) = withContext(Dispatchers.IO) {
|
||||||
|
|
@ -2195,13 +2242,15 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isDocumentReady, totalDisplayPages, displayMode) {
|
LaunchedEffect(isDocumentReady, totalDisplayPages, displayMode, currentBookId) {
|
||||||
if (isDocumentReady && !initialScrollDone) {
|
if (isDocumentReady && !initialScrollDone) {
|
||||||
val pageCount = totalDisplayPages
|
val pageCount = totalDisplayPages
|
||||||
if (pageCount <= 0) return@LaunchedEffect
|
if (pageCount <= 0) return@LaunchedEffect
|
||||||
|
|
||||||
val targetPage = pendingRestorePage?.coerceIn(0, pageCount - 1) ?: 0
|
val targetPage = pendingRestorePage?.coerceIn(0, pageCount - 1) ?: 0
|
||||||
Timber.tag("PdfPositionDebug").i("UI: Restoration Start | Target: $targetPage | Mode: $displayMode | Total: $pageCount")
|
Timber.tag("PdfPositionDebug").i("UI: Restoration Start | Target: $targetPage | Mode: $displayMode | Total: $pageCount | BookId: $currentBookId")
|
||||||
|
|
||||||
|
delay(100)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
when (displayMode) {
|
when (displayMode) {
|
||||||
|
|
@ -2209,21 +2258,35 @@ fun PdfViewerScreen(
|
||||||
if (pagerState.currentPage != targetPage) {
|
if (pagerState.currentPage != targetPage) {
|
||||||
pagerState.scrollToPage(targetPage)
|
pagerState.scrollToPage(targetPage)
|
||||||
}
|
}
|
||||||
initialScrollDone = true
|
|
||||||
}
|
}
|
||||||
DisplayMode.VERTICAL_SCROLL -> {
|
DisplayMode.VERTICAL_SCROLL -> {
|
||||||
while (verticalReaderState.snapToPageHandler == null) {
|
var attempts = 0
|
||||||
|
while (verticalReaderState.snapToPageHandler == null && attempts < 100) {
|
||||||
delay(16)
|
delay(16)
|
||||||
|
attempts++
|
||||||
}
|
}
|
||||||
|
if (verticalReaderState.snapToPageHandler != null) {
|
||||||
Timber.tag("PdfPositionDebug").d("UI: Executing Vertical snapToPage($targetPage)")
|
Timber.tag("PdfPositionDebug").d("UI: Executing Vertical snapToPage($targetPage)")
|
||||||
verticalReaderState.snapToPage(targetPage)
|
verticalReaderState.snapToPage(targetPage)
|
||||||
|
|
||||||
|
var waitAttempts = 0
|
||||||
|
while (verticalReaderState.currentPage != targetPage && waitAttempts < 20) {
|
||||||
|
delay(16)
|
||||||
|
waitAttempts++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.tag("PdfPositionDebug").w("UI: snapToPageHandler is null after timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(50)
|
||||||
initialScrollDone = true
|
initialScrollDone = true
|
||||||
}
|
Timber.tag("PdfPositionDebug").i("UI: Restoration Complete | Now at Page: $currentPage | initialScrollDone: $initialScrollDone")
|
||||||
}
|
|
||||||
Timber.tag("PdfPositionDebug").i("UI: Restoration Complete | Now at Page: $currentPage")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) {
|
if (e is CancellationException || e.javaClass.name.contains("CancellationException")) {
|
||||||
Timber.tag("PdfPositionDebug").w("UI: Restoration cancelled (likely new recomposition)")
|
Timber.tag("PdfPositionDebug").w("UI: Restoration cancelled (likely new recomposition)")
|
||||||
|
throw e
|
||||||
} else {
|
} else {
|
||||||
Timber.tag("PdfPositionDebug").e(e, "UI: Restoration error.")
|
Timber.tag("PdfPositionDebug").e(e, "UI: Restoration error.")
|
||||||
initialScrollDone = true
|
initialScrollDone = true
|
||||||
|
|
@ -2433,7 +2496,7 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.savePdfWithAnnotations(
|
viewModel.savePdfWithAnnotations(
|
||||||
sourceUri = pdfUri,
|
sourceUri = effectivePdfUri,
|
||||||
destUri = uri,
|
destUri = uri,
|
||||||
annotations = allAnnotations,
|
annotations = allAnnotations,
|
||||||
richTextPageLayouts = currentRichTextLayouts,
|
richTextPageLayouts = currentRichTextLayouts,
|
||||||
|
|
@ -2446,7 +2509,7 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveMode.ORIGINAL -> {
|
SaveMode.ORIGINAL -> {
|
||||||
viewModel.saveOriginalPdf(pdfUri, uri)
|
viewModel.saveOriginalPdf(effectivePdfUri, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|
@ -2773,6 +2836,7 @@ fun PdfViewerScreen(
|
||||||
fun isAnnotationHit(
|
fun isAnnotationHit(
|
||||||
annotation: PdfAnnotation,
|
annotation: PdfAnnotation,
|
||||||
hitPoint: PdfPoint,
|
hitPoint: PdfPoint,
|
||||||
|
lastHitPoint: PdfPoint?,
|
||||||
pageAspectRatio: Float,
|
pageAspectRatio: Float,
|
||||||
threshold: Float
|
threshold: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
@ -2781,11 +2845,37 @@ fun PdfViewerScreen(
|
||||||
val effectiveThreshold = threshold + (annotation.strokeWidth / 2f)
|
val effectiveThreshold = threshold + (annotation.strokeWidth / 2f)
|
||||||
val thresholdSq = effectiveThreshold * effectiveThreshold
|
val thresholdSq = effectiveThreshold * effectiveThreshold
|
||||||
|
|
||||||
|
fun distSqToEraser(px: Float, pyScaled: Float): Float {
|
||||||
|
val e1x = hitPoint.x
|
||||||
|
val e1yScaled = hitPoint.y / pageAspectRatio
|
||||||
|
if (lastHitPoint == null) {
|
||||||
|
val dx = px - e1x
|
||||||
|
val dy = pyScaled - e1yScaled
|
||||||
|
return dx * dx + dy * dy
|
||||||
|
}
|
||||||
|
val e0x = lastHitPoint.x
|
||||||
|
val e0yScaled = lastHitPoint.y / pageAspectRatio
|
||||||
|
|
||||||
|
val ex = e1x - e0x
|
||||||
|
val ey = e1yScaled - e0yScaled
|
||||||
|
val segLenSq = (ex * ex + ey * ey)
|
||||||
|
if (segLenSq < 1e-8f) {
|
||||||
|
val dx = px - e1x
|
||||||
|
val dy = pyScaled - e1yScaled
|
||||||
|
return dx * dx + dy * dy
|
||||||
|
}
|
||||||
|
val t = ((px - e0x) * ex + (pyScaled - e0yScaled) * ey) / segLenSq
|
||||||
|
val tClamped = t.coerceIn(0f, 1f)
|
||||||
|
val closestX = e0x + ex * tClamped
|
||||||
|
val closestY = e0yScaled + ey * tClamped
|
||||||
|
val dx = px - closestX
|
||||||
|
val dy = pyScaled - closestY
|
||||||
|
return dx * dx + dy * dy
|
||||||
|
}
|
||||||
|
|
||||||
if (annotation.points.size == 1) {
|
if (annotation.points.size == 1) {
|
||||||
val p = annotation.points[0]
|
val p = annotation.points[0]
|
||||||
val dx = (p.x - hitPoint.x)
|
return distSqToEraser(p.x, p.y / pageAspectRatio) < thresholdSq
|
||||||
val dy = (p.y - hitPoint.y) / pageAspectRatio
|
|
||||||
return (dx * dx + dy * dy) < thresholdSq
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i in 0 until annotation.points.size - 1) {
|
for (i in 0 until annotation.points.size - 1) {
|
||||||
|
|
@ -2807,6 +2897,11 @@ fun PdfViewerScreen(
|
||||||
val distSq = (pax - closestX) * (pax - closestX) + (pay - closestY) * (pay - closestY)
|
val distSq = (pax - closestX) * (pax - closestX) + (pay - closestY) * (pay - closestY)
|
||||||
|
|
||||||
if (distSq < thresholdSq) return true
|
if (distSq < thresholdSq) return true
|
||||||
|
|
||||||
|
if (lastHitPoint != null) {
|
||||||
|
if (distSqToEraser(a.x, a.y / pageAspectRatio) < thresholdSq) return true
|
||||||
|
if (distSqToEraser(b.x, b.y / pageAspectRatio) < thresholdSq) return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
@ -2878,7 +2973,7 @@ fun PdfViewerScreen(
|
||||||
val chunks = splitTextIntoChunks(textToChunk)
|
val chunks = splitTextIntoChunks(textToChunk)
|
||||||
|
|
||||||
val bookTitle = (pdfDocument as? PdfDocumentWrapper)?.pdfDocument?.getDocumentMeta()?.title?.takeIf { it.isNotBlank() }
|
val bookTitle = (pdfDocument as? PdfDocumentWrapper)?.pdfDocument?.getDocumentMeta()?.title?.takeIf { it.isNotBlank() }
|
||||||
?: pdfUri.lastPathSegment ?: "Document"
|
?: effectivePdfUri.lastPathSegment ?: "Document"
|
||||||
val pageTitle = "Page ${pageToRead + 1}"
|
val pageTitle = "Page ${pageToRead + 1}"
|
||||||
|
|
||||||
val ttsChunks = chunks.mapIndexed { index, text -> TtsChunk(text, "", index) }
|
val ttsChunks = chunks.mapIndexed { index, text -> TtsChunk(text, "", index) }
|
||||||
|
|
@ -3040,8 +3135,13 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(pdfUri, pdfiumCore, documentPassword) {
|
LaunchedEffect(effectivePdfUri, pdfiumCore, documentPassword) {
|
||||||
Timber.d("LaunchedEffect: Loading PDF document for URI: $pdfUri")
|
Timber.tag("PdfTabSync").i("UI: LaunchedEffect triggered by URI change: $effectivePdfUri")
|
||||||
|
|
||||||
|
Timber.tag("PdfTabSync").d("UI: Loading State -> activeTabBookId: ${uiState.activeTabBookId}, isLoading: $isLoadingDocument")
|
||||||
|
|
||||||
|
bookmarks = loadPdfBookmarksFromJson(uiState.initialBookmarksJson ?: initialBookmarksJson)
|
||||||
|
|
||||||
isLoadingDocument = true
|
isLoadingDocument = true
|
||||||
isDocumentReady = false
|
isDocumentReady = false
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
|
|
@ -3052,7 +3152,7 @@ fun PdfViewerScreen(
|
||||||
ocrUsedForCurrentPageTts = false
|
ocrUsedForCurrentPageTts = false
|
||||||
flatTableOfContents = emptyList()
|
flatTableOfContents = emptyList()
|
||||||
|
|
||||||
val fastId = getFastFileId(context, pdfUri)
|
val fastId = getFastFileId(context, effectivePdfUri)
|
||||||
val selectedId = uiState.selectedBookId
|
val selectedId = uiState.selectedBookId
|
||||||
|
|
||||||
if (selectedId != null && selectedId != fastId) {
|
if (selectedId != null && selectedId != fastId) {
|
||||||
|
|
@ -3063,43 +3163,47 @@ fun PdfViewerScreen(
|
||||||
currentBookId = fastId
|
currentBookId = fastId
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldDoc = pdfDocument
|
val cachedItem = documentCache.get(currentBookId!!)
|
||||||
val oldPfd = pfdState
|
if (cachedItem != null) {
|
||||||
|
Timber.tag("PdfTabSync").i("UI: Restoring from cache for $currentBookId")
|
||||||
|
pdfDocument = cachedItem.doc
|
||||||
|
pfdState = cachedItem.pfd
|
||||||
|
totalPages = cachedItem.totalPages
|
||||||
|
pageAspectRatios = cachedItem.pageAspectRatios
|
||||||
|
flatTableOfContents = cachedItem.flatTableOfContents
|
||||||
|
|
||||||
|
val mapPage = tabStateMap[currentBookId!!]
|
||||||
|
val uiPage = uiState.initialPageInBook
|
||||||
|
Timber.tag("PdfTabSync").d("UI: Restoring position | tabStateMap=$mapPage, uiState=$uiPage, initialPage=$initialPage")
|
||||||
|
|
||||||
|
pendingRestorePage = mapPage ?: uiPage ?: initialPage
|
||||||
|
initialScrollDone = false
|
||||||
|
isDocumentReady = true
|
||||||
|
isLoadingDocument = false
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
val mapPageInit = tabStateMap[currentBookId!!]
|
||||||
|
val uiPageInit = uiState.initialPageInBook
|
||||||
|
Timber.tag("PdfTabSync").d("UI: Initial position | tabStateMap=$mapPageInit, uiState=$uiPageInit, initialPage=$initialPage")
|
||||||
|
pendingRestorePage = mapPageInit ?: uiPageInit ?: initialPage
|
||||||
|
initialScrollDone = false
|
||||||
|
|
||||||
pdfDocument = null
|
pdfDocument = null
|
||||||
pfdState = null
|
pfdState = null
|
||||||
totalPages = 0
|
totalPages = 0
|
||||||
|
|
||||||
if (oldDoc != null || oldPfd != null) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
oldDoc?.let {
|
|
||||||
try {
|
|
||||||
it.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oldPfd?.let {
|
|
||||||
try {
|
|
||||||
it.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentPfdOpened: ParcelFileDescriptor? = null
|
var currentPfdOpened: ParcelFileDescriptor? = null
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Timber.d("Opening ParcelFileDescriptor for URI: $pdfUri")
|
Timber.tag("PdfTabSync").v("UI: Opening PFD for $effectivePdfUri")
|
||||||
|
|
||||||
if (pdfUri.scheme != "opds-pse") {
|
if (pdfUri.scheme != "opds-pse") {
|
||||||
currentPfdOpened = context.contentResolver.openFileDescriptor(pdfUri, "r")
|
currentPfdOpened = context.contentResolver.openFileDescriptor(effectivePdfUri, "r")
|
||||||
if (currentPfdOpened == null) throw Exception("Failed to open ParcelFileDescriptor")
|
if (currentPfdOpened == null) throw Exception("Failed to open ParcelFileDescriptor")
|
||||||
}
|
}
|
||||||
|
|
||||||
val doc = DocumentFactory.loadDocument(context, pdfUri, uiState.selectedFileType ?: FileType.PDF, documentPassword, pdfiumCore)
|
val doc = DocumentFactory.loadDocument(context, effectivePdfUri, uiState.selectedFileType ?: FileType.PDF, documentPassword, pdfiumCore)
|
||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
doc.close()
|
doc.close()
|
||||||
|
|
@ -3180,6 +3284,17 @@ fun PdfViewerScreen(
|
||||||
isDocumentReady = true
|
isDocumentReady = true
|
||||||
isLoadingDocument = false
|
isLoadingDocument = false
|
||||||
|
|
||||||
|
documentCache.put(
|
||||||
|
currentBookId!!,
|
||||||
|
DocumentCacheItem(
|
||||||
|
doc = doc,
|
||||||
|
pfd = currentPfdOpened!!,
|
||||||
|
totalPages = pagesCount,
|
||||||
|
pageAspectRatios = ratios,
|
||||||
|
flatTableOfContents = flatTableOfContents
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
showPasswordDialog = false
|
showPasswordDialog = false
|
||||||
isPasswordError = false
|
isPasswordError = false
|
||||||
|
|
@ -3220,9 +3335,11 @@ fun PdfViewerScreen(
|
||||||
isLoadingDocument = false
|
isLoadingDocument = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.i("PDF document loaded optimistically. Total Pages: $totalPages.")
|
Timber.tag("PdfTabSync").v("UI: Pdfium Document created. Page count: $pagesCount")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException || e.javaClass.name.contains("CancellationException")) throw e
|
||||||
|
Timber.tag("PdfTabSync").e(e, "UI: Error in load effect for $effectivePdfUri")
|
||||||
val errorString = e.toString()
|
val errorString = e.toString()
|
||||||
val causeString = e.cause?.toString() ?: ""
|
val causeString = e.cause?.toString() ?: ""
|
||||||
|
|
||||||
|
|
@ -3297,28 +3414,22 @@ fun PdfViewerScreen(
|
||||||
ttsController.stop()
|
ttsController.stop()
|
||||||
PdfBitmapPool.clear()
|
PdfBitmapPool.clear()
|
||||||
PdfThumbnailCache.clear()
|
PdfThumbnailCache.clear()
|
||||||
|
documentCache.evictAll()
|
||||||
|
|
||||||
val docToClose = pdfDocument
|
val docToClose = pdfDocument
|
||||||
val pfdToClose = pfdState
|
val pfdToClose = pfdState
|
||||||
pdfDocument = null
|
pdfDocument = null
|
||||||
pfdState = null
|
pfdState = null
|
||||||
|
|
||||||
if (docToClose != null || pfdToClose != null) {
|
if (docToClose != null || pfdToClose != null) {
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
docToClose?.let {
|
docToClose?.let {
|
||||||
Timber.d("Closing PDF document in onDispose.")
|
Timber.d("Closing PDF document in onDispose.")
|
||||||
try {
|
try { it.close() } catch (e: Exception) { Timber.e(e, "Error closing document") }
|
||||||
it.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error closing document in onDispose")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pfdToClose?.let {
|
pfdToClose?.let {
|
||||||
Timber.d("Closing ParcelFileDescriptor in onDispose: $it")
|
Timber.d("Closing ParcelFileDescriptor in onDispose: $it")
|
||||||
try {
|
try { it.close() } catch (e: Exception) { Timber.e(e, "Error closing ParcelFileDescriptor") }
|
||||||
it.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error closing ParcelFileDescriptor in onDispose")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3329,7 +3440,7 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
var isOcrScanning by remember { mutableStateOf(false) }
|
var isOcrScanning by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(pdfUri, currentBookId, totalPages) {
|
LaunchedEffect(effectivePdfUri, currentBookId, totalPages) {
|
||||||
if (currentBookId == null || totalPages == 0) return@LaunchedEffect
|
if (currentBookId == null || totalPages == 0) return@LaunchedEffect
|
||||||
if (isBackgroundIndexing && backgroundIndexingProgress > 0f) return@LaunchedEffect
|
if (isBackgroundIndexing && backgroundIndexingProgress > 0f) return@LaunchedEffect
|
||||||
if (uiState.selectedFileType != FileType.PDF) return@LaunchedEffect
|
if (uiState.selectedFileType != FileType.PDF) return@LaunchedEffect
|
||||||
|
|
@ -3359,7 +3470,7 @@ fun PdfViewerScreen(
|
||||||
"Indexer: Starting background indexing for ${totalPages - existingPages.size} pages."
|
"Indexer: Starting background indexing for ${totalPages - existingPages.size} pages."
|
||||||
)
|
)
|
||||||
|
|
||||||
bgPfd = context.contentResolver.openFileDescriptor(pdfUri, "r")
|
bgPfd = context.contentResolver.openFileDescriptor(effectivePdfUri, "r")
|
||||||
if (bgPfd != null) {
|
if (bgPfd != null) {
|
||||||
bgDoc = pdfiumCore.newDocument(bgPfd, documentPassword)
|
bgDoc = pdfiumCore.newDocument(bgPfd, documentPassword)
|
||||||
|
|
||||||
|
|
@ -4223,6 +4334,13 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
val currentSelectedTool by rememberUpdatedState(selectedTool)
|
val currentSelectedTool by rememberUpdatedState(selectedTool)
|
||||||
|
|
||||||
|
val currentStrokeColorState by rememberUpdatedState(
|
||||||
|
currentStrokeColor
|
||||||
|
)
|
||||||
|
val currentStrokeWidthState by rememberUpdatedState(
|
||||||
|
currentStrokeWidth
|
||||||
|
)
|
||||||
|
|
||||||
@Suppress("ControlFlowWithEmptyBody") val onDrawPagination =
|
@Suppress("ControlFlowWithEmptyBody") val onDrawPagination =
|
||||||
remember(pageIndex) {
|
remember(pageIndex) {
|
||||||
{ point: PdfPoint ->
|
{ point: PdfPoint ->
|
||||||
|
|
@ -4231,8 +4349,9 @@ fun PdfViewerScreen(
|
||||||
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
||||||
val existing = allAnnotations[pageIndex] ?: emptyList()
|
val existing = allAnnotations[pageIndex] ?: emptyList()
|
||||||
val toRemove = existing.filter {
|
val toRemove = existing.filter {
|
||||||
isAnnotationHit(it, point, aspectRatio, activeToolThickness)
|
isAnnotationHit(it, point, lastEraserPoint, aspectRatio, currentStrokeWidthState)
|
||||||
}
|
}
|
||||||
|
lastEraserPoint = point
|
||||||
if (toRemove.isNotEmpty()) {
|
if (toRemove.isNotEmpty()) {
|
||||||
val batch =
|
val batch =
|
||||||
erasedAnnotationsFromStroke.getOrPut(
|
erasedAnnotationsFromStroke.getOrPut(
|
||||||
|
|
@ -4259,13 +4378,6 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentStrokeColorState by rememberUpdatedState(
|
|
||||||
currentStrokeColor
|
|
||||||
)
|
|
||||||
val currentStrokeWidthState by rememberUpdatedState(
|
|
||||||
currentStrokeWidth
|
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("ControlFlowWithEmptyBody") val onDrawStartPagination =
|
@Suppress("ControlFlowWithEmptyBody") val onDrawStartPagination =
|
||||||
remember(pageIndex) {
|
remember(pageIndex) {
|
||||||
{ point: PdfPoint ->
|
{ point: PdfPoint ->
|
||||||
|
|
@ -4274,11 +4386,12 @@ fun PdfViewerScreen(
|
||||||
} else {
|
} else {
|
||||||
if (currentSelectedTool == InkType.TEXT) {
|
if (currentSelectedTool == InkType.TEXT) {
|
||||||
} else if (currentSelectedTool == InkType.ERASER) {
|
} else if (currentSelectedTool == InkType.ERASER) {
|
||||||
|
lastEraserPoint = point
|
||||||
erasedAnnotationsFromStroke.clear()
|
erasedAnnotationsFromStroke.clear()
|
||||||
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
||||||
val existing = allAnnotations[pageIndex] ?: emptyList()
|
val existing = allAnnotations[pageIndex] ?: emptyList()
|
||||||
val toRemove = existing.filter {
|
val toRemove = existing.filter {
|
||||||
isAnnotationHit(it, point, aspectRatio, activeToolThickness)
|
isAnnotationHit(it, point, lastEraserPoint, aspectRatio, currentStrokeWidthState)
|
||||||
}
|
}
|
||||||
if (toRemove.isNotEmpty()) {
|
if (toRemove.isNotEmpty()) {
|
||||||
val batch =
|
val batch =
|
||||||
|
|
@ -4617,12 +4730,13 @@ fun PdfViewerScreen(
|
||||||
} else {
|
} else {
|
||||||
if (currentSelectedTool == InkType.TEXT) {
|
if (currentSelectedTool == InkType.TEXT) {
|
||||||
} else if (currentSelectedTool == InkType.ERASER) {
|
} else if (currentSelectedTool == InkType.ERASER) {
|
||||||
|
lastEraserPoint = point
|
||||||
erasedAnnotationsFromStroke.clear()
|
erasedAnnotationsFromStroke.clear()
|
||||||
|
|
||||||
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
||||||
val existing = allAnnotations[pageIndex] ?: emptyList()
|
val existing = allAnnotations[pageIndex] ?: emptyList()
|
||||||
val toRemove = existing.filter {
|
val toRemove = existing.filter {
|
||||||
isAnnotationHit(it, point, aspectRatio, activeToolThickness)
|
isAnnotationHit(it, point, lastEraserPoint, aspectRatio, currentStrokeWidthState)
|
||||||
}
|
}
|
||||||
if (toRemove.isNotEmpty()) {
|
if (toRemove.isNotEmpty()) {
|
||||||
val batch =
|
val batch =
|
||||||
|
|
@ -4660,8 +4774,9 @@ fun PdfViewerScreen(
|
||||||
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
|
||||||
val existing = allAnnotations[pageIndex] ?: emptyList()
|
val existing = allAnnotations[pageIndex] ?: emptyList()
|
||||||
val toRemove = existing.filter {
|
val toRemove = existing.filter {
|
||||||
isAnnotationHit(it, point, aspectRatio, activeToolThickness)
|
isAnnotationHit(it, point, lastEraserPoint, aspectRatio, currentStrokeWidthState)
|
||||||
}
|
}
|
||||||
|
lastEraserPoint = point
|
||||||
if (toRemove.isNotEmpty()) {
|
if (toRemove.isNotEmpty()) {
|
||||||
val batch =
|
val batch =
|
||||||
erasedAnnotationsFromStroke.getOrPut(
|
erasedAnnotationsFromStroke.getOrPut(
|
||||||
|
|
@ -5229,15 +5344,13 @@ fun PdfViewerScreen(
|
||||||
modifier = Modifier.align(Alignment.TopCenter)
|
modifier = Modifier.align(Alignment.TopCenter)
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.height(56.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
tonalElevation = 4.dp
|
tonalElevation = 4.dp
|
||||||
) {
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().height(56.dp)
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 4.dp),
|
.padding(horizontal = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|
@ -5255,8 +5368,7 @@ fun PdfViewerScreen(
|
||||||
TooltipIconButton(
|
TooltipIconButton(
|
||||||
text = stringResource(R.string.tooltip_back),
|
text = stringResource(R.string.tooltip_back),
|
||||||
description = stringResource(R.string.tooltip_back_desc),
|
description = stringResource(R.string.tooltip_back_desc),
|
||||||
onClick = { saveStateAndExit() }
|
onClick = { saveStateAndExit() }) {
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
|
|
@ -5280,17 +5392,14 @@ fun PdfViewerScreen(
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(start = 12.dp).weight(1f)
|
||||||
.padding(start = 12.dp)
|
|
||||||
.weight(1f)
|
|
||||||
.testTag("PageNumberIndicator")
|
.testTag("PageNumberIndicator")
|
||||||
)
|
)
|
||||||
|
|
||||||
TooltipIconButton(
|
TooltipIconButton(
|
||||||
text = "Theme",
|
text = "Theme",
|
||||||
description = "Theme Settings",
|
description = "Theme Settings",
|
||||||
onClick = { showThemePanel = true }
|
onClick = { showThemePanel = true }) {
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.palette),
|
painter = painterResource(id = R.drawable.palette),
|
||||||
contentDescription = "Theme Settings",
|
contentDescription = "Theme Settings",
|
||||||
|
|
@ -5299,19 +5408,14 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
TooltipIconButton(
|
TooltipIconButton(
|
||||||
text = if (isScrollLocked)
|
text = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan)
|
||||||
stringResource(R.string.tooltip_unlock_pan)
|
else stringResource(R.string.tooltip_lock_pan),
|
||||||
else
|
description = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan_desc)
|
||||||
stringResource(R.string.tooltip_lock_pan),
|
else stringResource(R.string.tooltip_lock_pan_desc),
|
||||||
description = if (isScrollLocked)
|
|
||||||
stringResource(R.string.tooltip_unlock_pan_desc)
|
|
||||||
else
|
|
||||||
stringResource(R.string.tooltip_lock_pan_desc),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
isScrollLocked = !isScrollLocked
|
isScrollLocked = !isScrollLocked
|
||||||
savePdfScrollLocked(context, bookId, isScrollLocked)
|
savePdfScrollLocked(context, bookId, isScrollLocked)
|
||||||
}
|
}) {
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen,
|
imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen,
|
||||||
contentDescription = if (isScrollLocked) "Unlock Panning" else "Lock Panning",
|
contentDescription = if (isScrollLocked) "Unlock Panning" else "Lock Panning",
|
||||||
|
|
@ -5325,8 +5429,7 @@ fun PdfViewerScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
isFullScreen = true
|
isFullScreen = true
|
||||||
savePdfFullScreen(context, bookId, true)
|
savePdfFullScreen(context, bookId, true)
|
||||||
}
|
}) {
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Fullscreen,
|
imageVector = Icons.Default.Fullscreen,
|
||||||
contentDescription = "Enter Full Screen",
|
contentDescription = "Enter Full Screen",
|
||||||
|
|
@ -5337,8 +5440,7 @@ fun PdfViewerScreen(
|
||||||
TooltipIconButton(
|
TooltipIconButton(
|
||||||
text = stringResource(R.string.tooltip_dictionary),
|
text = stringResource(R.string.tooltip_dictionary),
|
||||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||||
onClick = { showDictionarySettingsSheet = true }
|
onClick = { showDictionarySettingsSheet = true }) {
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.dictionary),
|
painter = painterResource(id = R.drawable.dictionary),
|
||||||
contentDescription = "Dictionary Settings",
|
contentDescription = "Dictionary Settings",
|
||||||
|
|
@ -5347,7 +5449,9 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
TooltipIconButton(text = "Pen Playground", onClick = { showPenPlayground = true }) {
|
TooltipIconButton(
|
||||||
|
text = "Pen Playground",
|
||||||
|
onClick = { showPenPlayground = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Star,
|
imageVector = Icons.Default.Star,
|
||||||
contentDescription = "Open Pen Playground",
|
contentDescription = "Open Pen Playground",
|
||||||
|
|
@ -5356,10 +5460,12 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
TooltipIconButton(text = "Import SVG", onClick = {
|
TooltipIconButton(text = "Import SVG", onClick = {
|
||||||
val page = if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage
|
val page =
|
||||||
|
if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
val svgAnnotations = SvgToAnnotationConverter.importSvgFromAssets(
|
val svgAnnotations =
|
||||||
|
SvgToAnnotationConverter.importSvgFromAssets(
|
||||||
context = context,
|
context = context,
|
||||||
fileName = "demo_art.svg",
|
fileName = "demo_art.svg",
|
||||||
pageIndex = page
|
pageIndex = page
|
||||||
|
|
@ -5367,11 +5473,18 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (svgAnnotations.isNotEmpty()) {
|
if (svgAnnotations.isNotEmpty()) {
|
||||||
val existing = allAnnotations[page] ?: emptyList()
|
val existing =
|
||||||
allAnnotations = allAnnotations + (page to (existing + svgAnnotations))
|
allAnnotations[page] ?: emptyList()
|
||||||
|
allAnnotations =
|
||||||
|
allAnnotations + (page to (existing + svgAnnotations))
|
||||||
|
|
||||||
svgAnnotations.forEach { annot ->
|
svgAnnotations.forEach { annot ->
|
||||||
undoStack.add(HistoryAction.Add(page, annot))
|
undoStack.add(
|
||||||
|
HistoryAction.Add(
|
||||||
|
page,
|
||||||
|
annot
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
redoStack.clear()
|
redoStack.clear()
|
||||||
|
|
||||||
|
|
@ -5395,8 +5508,7 @@ fun PdfViewerScreen(
|
||||||
TooltipIconButton(
|
TooltipIconButton(
|
||||||
text = stringResource(R.string.tooltip_more_options),
|
text = stringResource(R.string.tooltip_more_options),
|
||||||
description = stringResource(R.string.tooltip_more_options_desc),
|
description = stringResource(R.string.tooltip_more_options_desc),
|
||||||
onClick = { showMoreMenu = true }
|
onClick = { showMoreMenu = true }) {
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = "More Options"
|
contentDescription = "More Options"
|
||||||
|
|
@ -5414,8 +5526,7 @@ fun PdfViewerScreen(
|
||||||
showMoreMenu = false
|
showMoreMenu = false
|
||||||
hasSelectedOcrLanguage = true
|
hasSelectedOcrLanguage = true
|
||||||
showOcrLanguageDialog = true
|
showOcrLanguageDialog = true
|
||||||
}
|
})
|
||||||
)
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5465,8 +5576,7 @@ fun PdfViewerScreen(
|
||||||
contentDescription = "Selected"
|
contentDescription = "Selected"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Auto Scroll") },
|
text = { Text("Auto Scroll") },
|
||||||
|
|
@ -5476,8 +5586,7 @@ fun PdfViewerScreen(
|
||||||
isAutoScrollModeActive = true
|
isAutoScrollModeActive = true
|
||||||
isAutoScrollPlaying = true
|
isAutoScrollPlaying = true
|
||||||
showBars = !isMusicianMode
|
showBars = !isMusicianMode
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
|
|
@ -5492,8 +5601,7 @@ fun PdfViewerScreen(
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
|
|
@ -5503,9 +5611,12 @@ fun PdfViewerScreen(
|
||||||
showTtsSettingsSheet = true
|
showTtsSettingsSheet = true
|
||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(painter = painterResource(id = R.drawable.text_to_speech), contentDescription = null, modifier = Modifier.size(20.dp))
|
Icon(
|
||||||
}
|
painter = painterResource(id = R.drawable.text_to_speech),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
@ -5564,20 +5675,25 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
saveAllData(true).join()
|
saveAllData(true).join()
|
||||||
|
|
||||||
val resolvedPage = if (!initialScrollDone && currentPage == 0) {
|
val resolvedPage =
|
||||||
|
if (!initialScrollDone && currentPage == 0) {
|
||||||
pendingRestorePage ?: 0
|
pendingRestorePage ?: 0
|
||||||
} else {
|
} else {
|
||||||
currentPage
|
currentPage
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasReflowFile) {
|
if (hasReflowFile) {
|
||||||
val item = uiState.allRecentFiles.find { it.bookId == reflowBookId }
|
val item =
|
||||||
|
uiState.allRecentFiles.find { it.bookId == reflowBookId }
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
viewModel.switchToFileSeamlessly(item, resolvedPage)
|
viewModel.switchToFileSeamlessly(
|
||||||
|
item,
|
||||||
|
resolvedPage
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
viewModel.generateAndImportReflowFile(
|
viewModel.generateAndImportReflowFile(
|
||||||
pdfBookId = bookId,
|
pdfBookId = bookId,
|
||||||
pdfUri = pdfUri,
|
pdfUri = effectivePdfUri,
|
||||||
originalTitle = originalFileName,
|
originalTitle = originalFileName,
|
||||||
autoOpenPage = resolvedPage
|
autoOpenPage = resolvedPage
|
||||||
)
|
)
|
||||||
|
|
@ -5585,7 +5701,7 @@ fun PdfViewerScreen(
|
||||||
} else {
|
} else {
|
||||||
viewModel.generateAndImportReflowFile(
|
viewModel.generateAndImportReflowFile(
|
||||||
pdfBookId = bookId,
|
pdfBookId = bookId,
|
||||||
pdfUri = pdfUri,
|
pdfUri = effectivePdfUri,
|
||||||
originalTitle = originalFileName,
|
originalTitle = originalFileName,
|
||||||
autoOpenPage = resolvedPage
|
autoOpenPage = resolvedPage
|
||||||
)
|
)
|
||||||
|
|
@ -5598,8 +5714,7 @@ fun PdfViewerScreen(
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
|
|
@ -5626,10 +5741,13 @@ fun PdfViewerScreen(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (uiState.selectedFileType == FileType.PDF) {
|
if (uiState.selectedFileType == FileType.PDF) {
|
||||||
DropdownMenuItem(text = { Text("Print") }, onClick = {
|
DropdownMenuItem(
|
||||||
|
text = { Text("Print") },
|
||||||
|
onClick = {
|
||||||
showMoreMenu = false
|
showMoreMenu = false
|
||||||
onPrintDocument()
|
onPrintDocument()
|
||||||
}, leadingIcon = {
|
},
|
||||||
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.print),
|
painter = painterResource(id = R.drawable.print),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
@ -5641,6 +5759,84 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isTabsEnabled && openTabs.isNotEmpty() && effectiveFileType == FileType.PDF) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(44.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)),
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
items(openTabs, key = { it.bookId }) { tab ->
|
||||||
|
val isSelected = tab.bookId == activeTabBookId
|
||||||
|
val bgColor = if (isSelected) MaterialTheme.colorScheme.surface else Color.Transparent
|
||||||
|
val contentColor = if (isSelected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(if (isSelected) 44.dp else 36.dp)
|
||||||
|
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||||
|
.background(bgColor)
|
||||||
|
.clickable {
|
||||||
|
Timber.tag("PdfTabSync").i("UI: Tab clicked: ${tab.bookId}")
|
||||||
|
if (tab.bookId != activeTabBookId) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
Timber.tag("PdfTabSync").d("UI: Dispatching switchTab for ${tab.bookId}")
|
||||||
|
currentBookId?.let { tabStateMap[it] = currentPage }
|
||||||
|
saveAllData(true).join()
|
||||||
|
viewModel.switchTab(tab.bookId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.tag("PdfTabSync").d("UI: Ignored click - Tab already active.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Tab Title
|
||||||
|
Text(
|
||||||
|
text = tab.customName ?: tab.title ?: tab.displayName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.widthIn(max = 140.dp),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (isSelected) saveAllData(true).join()
|
||||||
|
viewModel.closeTab(tab.bookId)
|
||||||
|
if (isSelected && openTabs.size == 1) {
|
||||||
|
onNavigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Close Tab", modifier = Modifier.size(16.dp), tint = contentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
IconButton(
|
||||||
|
onClick = { showNewTabSheet = true },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp, bottom = 4.dp)
|
||||||
|
.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = "New Tab",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6168,8 +6364,6 @@ fun PdfViewerScreen(
|
||||||
val popupPlacementConfig =
|
val popupPlacementConfig =
|
||||||
remember(dockLocation, dockOffset, boxMaxHeightFloat, dockHeightPx) {
|
remember(dockLocation, dockOffset, boxMaxHeightFloat, dockHeightPx) {
|
||||||
val margin = 16.dp
|
val margin = 16.dp
|
||||||
with(density) { boxMaxHeightFloat.toDp() }
|
|
||||||
|
|
||||||
val dockTopY = when (dockLocation) {
|
val dockTopY = when (dockLocation) {
|
||||||
DockLocation.TOP -> 0f
|
DockLocation.TOP -> 0f
|
||||||
DockLocation.BOTTOM -> boxMaxHeightFloat - dockHeightPx
|
DockLocation.BOTTOM -> boxMaxHeightFloat - dockHeightPx
|
||||||
|
|
@ -6183,10 +6377,10 @@ fun PdfViewerScreen(
|
||||||
if (isDockInBottomHalf) {
|
if (isDockInBottomHalf) {
|
||||||
val distFromBottom = boxMaxHeightFloat - dockTopY
|
val distFromBottom = boxMaxHeightFloat - dockTopY
|
||||||
val paddingBottom = with(density) { distFromBottom.toDp() } + margin
|
val paddingBottom = with(density) { distFromBottom.toDp() } + margin
|
||||||
Triple(Alignment.BottomCenter, 0.dp, paddingBottom)
|
Triple(Alignment.BottomCenter, 0.dp, paddingBottom.coerceAtLeast(0.dp))
|
||||||
} else {
|
} else {
|
||||||
val paddingTop = with(density) { dockBottomY.toDp() } + margin
|
val paddingTop = with(density) { dockBottomY.toDp() } + margin
|
||||||
Triple(Alignment.TopCenter, paddingTop, 0.dp)
|
Triple(Alignment.TopCenter, paddingTop.coerceAtLeast(0.dp), 0.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6804,6 +6998,57 @@ fun PdfViewerScreen(
|
||||||
onConfirm = { password -> documentPassword = password })
|
onConfirm = { password -> documentPassword = password })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showNewTabSheet) {
|
||||||
|
androidx.compose.material3.ModalBottomSheet(
|
||||||
|
onDismissRequest = { showNewTabSheet = false },
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
val pdfFiles = remember(uiState.rawLibraryFiles, openTabs) {
|
||||||
|
val openIds = openTabs.map { it.bookId }
|
||||||
|
uiState.rawLibraryFiles
|
||||||
|
.filter { it.type == FileType.PDF && it.bookId !in openIds }
|
||||||
|
.sortedByDescending { it.timestamp }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Add PDF to Tab",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
if (pdfFiles.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
"No other PDFs found in your library.",
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
items(pdfFiles, key = { it.bookId }) { file ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(file.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
|
supportingContent = { file.author?.let { Text(it, maxLines = 1, overflow = TextOverflow.Ellipsis) } },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
sheetState.hide()
|
||||||
|
showNewTabSheet = false
|
||||||
|
viewModel.switchTab(file.bookId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showPenPlayground) {
|
if (showPenPlayground) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -7109,7 +7354,7 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
viewModel.sharePdf(
|
viewModel.sharePdf(
|
||||||
activityContext = context,
|
activityContext = context,
|
||||||
sourceUri = pdfUri,
|
sourceUri = effectivePdfUri,
|
||||||
annotations = allAnnotations,
|
annotations = allAnnotations,
|
||||||
richTextPageLayouts = currentRichTextLayouts,
|
richTextPageLayouts = currentRichTextLayouts,
|
||||||
textBoxes = textBoxes.toList(),
|
textBoxes = textBoxes.toList(),
|
||||||
|
|
|
||||||
|
|
@ -625,4 +625,19 @@
|
||||||
<string name="error_opening_dictionary">Error opening dictionary</string>
|
<string name="error_opening_dictionary">Error opening dictionary</string>
|
||||||
<string name="error_opening_translate">Error opening translate app</string>
|
<string name="error_opening_translate">Error opening translate app</string>
|
||||||
<string name="error_opening_search">Error opening search 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>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue