Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ android {
applicationId 'com.builderpro.opennotes'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 15
versionName "1.3.2"
versionCode 16
versionName "1.3.3"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.builderpro.opennotes

import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Rect
import android.graphics.pdf.PdfDocument
import android.graphics.pdf.PdfRenderer
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.Base64
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
Expand Down Expand Up @@ -84,6 +89,66 @@ class PDFUtilsModule(private val reactContext: ReactApplicationContext) :
}
}

/**
* Concatenates PDFs into one new cache file and resolves its file:// URI.
* Android has no vector PDF merge API, so each page is rendered at 2x and
* written into a new document one page at a time to keep memory bounded.
*/
@ReactMethod
fun mergePdfFiles(fileUris: ReadableArray, promise: Promise) {
if (fileUris.size() == 0) {
promise.reject("E_NO_INPUT", "No PDF files to merge")
return
}
val exportDir = File(reactContext.cacheDir, "pdf-export")
val destination = File(exportDir, "${UUID.randomUUID()}.pdf")
val output = PdfDocument()
try {
// Earlier exports have been shared already; don't let them pile up.
exportDir.listFiles()?.forEach { it.delete() }
exportDir.mkdirs()
var outputPageNumber = 1
for (index in 0 until fileUris.size()) {
val uri = fileUris.getString(index)
?: throw IOException("Missing PDF file URL at position $index")
val source = localFileFor(uri) ?: throw IOException("Invalid PDF file URL: $uri")
if (!source.exists()) throw IOException("Could not read ${source.name}")
ParcelFileDescriptor.open(source, ParcelFileDescriptor.MODE_READ_ONLY).use { pfd ->
PdfRenderer(pfd).use { renderer ->
for (pageIndex in 0 until renderer.pageCount) {
renderer.openPage(pageIndex).use { page ->
copyPage(page, output, outputPageNumber)
}
outputPageNumber += 1
}
}
}
}
FileOutputStream(destination).use { output.writeTo(it) }
promise.resolve("file://${destination.absolutePath}")
} catch (e: Exception) {
destination.delete()
promise.reject("E_MERGE_FAILED", e.message ?: "Could not merge PDF files", e)
} finally {
output.close()
}
}

private fun copyPage(page: PdfRenderer.Page, output: PdfDocument, pageNumber: Int) {
val scale = 2
val bitmap = Bitmap.createBitmap(page.width * scale, page.height * scale, Bitmap.Config.ARGB_8888)
try {
bitmap.eraseColor(Color.WHITE)
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT)
val info = PdfDocument.PageInfo.Builder(page.width, page.height, pageNumber).create()
val outputPage = output.startPage(info)
outputPage.canvas.drawBitmap(bitmap, null, Rect(0, 0, page.width, page.height), null)
output.finishPage(outputPage)
} finally {
bitmap.recycle()
}
}

private fun getPageCountFromFile(file: File): Int? {
if (!file.exists()) return null
val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
Expand Down Expand Up @@ -114,6 +179,13 @@ class PDFUtilsModule(private val reactContext: ReactApplicationContext) :
}
}

/** A local file for a file:// URI (percent-decoded) or a plain absolute path. */
private fun localFileFor(uri: String): File? = when {
uri.startsWith("file://") -> File(Uri.parse(uri).path ?: uri.removePrefix("file://"))
uri.startsWith("/") -> File(uri)
else -> null
}

private fun copySourceToTempFile(sourceUrl: String): File {
val importDir = File(reactContext.cacheDir, "pdf-import")
if (!importDir.exists() && !importDir.mkdirs()) {
Expand All @@ -128,12 +200,9 @@ class PDFUtilsModule(private val reactContext: ReactApplicationContext) :
reactContext.contentResolver.openInputStream(Uri.parse(sourceUrl))
?: throw IOException("Could not open content URI")
}
sourceUrl.startsWith("file://") -> {
val path = Uri.parse(sourceUrl).path ?: sourceUrl.removePrefix("file://")
FileInputStream(File(path))
}
sourceUrl.startsWith("/") -> FileInputStream(File(sourceUrl))
else -> throw IOException("Unsupported file path format: $sourceUrl")
else -> FileInputStream(
localFileFor(sourceUrl) ?: throw IOException("Unsupported file path format: $sourceUrl"),
)
}

inputStream.use { input ->
Expand Down
6 changes: 3 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "OpenNotes",
"slug": "open-notes",
"version": "1.3.2",
"version": "1.3.3",
"scheme": "opennotes",
"orientation": "default",
"icon": "./assets/icon.png",
Expand All @@ -16,7 +16,7 @@
"ios": {
"bundleIdentifier": "com.builderpro.opennotes",
"icon": "./assets/icon.png",
"buildNumber": "15",
"buildNumber": "16",
"supportsTablet": true,
"usesIcloudStorage": true,
"entitlements": {
Expand Down Expand Up @@ -65,7 +65,7 @@
},
"android": {
"package": "com.builderpro.opennotes",
"versionCode": 15,
"versionCode": 16,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#F7F7F4"
Expand Down
9 changes: 9 additions & 0 deletions app/+native-intent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// "Open In" launches the app with the PDF's file:// (or Android content://)
// URL. That is not a screen route: map it to the library before routing so
// expo-router never shows its "Unmatched Route" page. RootLayout's Open-In
// handler still receives the original URL through expo-linking and imports
// the PDF.
export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
const lower = path.toLowerCase();
return lower.startsWith('file:') || lower.startsWith('content:') ? '/' : path;
}
6 changes: 6 additions & 0 deletions app/+not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Redirect } from 'expo-router';

/** Unknown deep links open the library instead of a developer error page. */
export default function NotFound() {
return <Redirect href="/" />;
}
90 changes: 87 additions & 3 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import 'react-native-gesture-handler';
import React, { useEffect, useRef } from 'react';
import { Stack, useRouter } from 'expo-router';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Stack, useRouter, type ErrorBoundaryProps } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Alert, useColorScheme, View } from 'react-native';
import { Alert, Pressable, Share, StyleSheet, Text, useColorScheme, View } from 'react-native';
import { CrashReportBanner } from '../src/components/CrashReportBanner';
import * as Linking from 'expo-linking';
import { darkColors, lightColors } from '../src/theme/colors';
import { radius, spacing } from '../src/theme/spacing';
import { typography } from '../src/theme/typography';
import { useTheme } from '../src/hooks/useTheme';
import { createPdfNoteFromUri } from '../src/services/pdfImportService';
import {
installCrashCapture,
recordCrash,
takePendingCrashReport,
} from '../src/services/crashCapture';
import { formatCrashReport, type CrashReport } from '../src/services/crashReport';
import { t } from '../src/i18n';

installCrashCapture();
// Read once per launch; reading also removes the stored report.
const reportFromPreviousRun = takePendingCrashReport();

const handledPdfUrls = new Set<string>();

Expand All @@ -16,6 +31,12 @@ export default function RootLayout() {
const isDark = scheme === 'dark';
const colors = isDark ? darkColors : lightColors;
useOpenInPdfImport();
const [pendingReport, setPendingReport] = useState<CrashReport | null>(reportFromPreviousRun);
const dismissReport = useCallback(() => setPendingReport(null), []);
const shareReport = useCallback(() => {
if (pendingReport) shareCrashReport(pendingReport);
setPendingReport(null);
}, [pendingReport]);
return (
<GestureHandlerRootView style={{ flex: 1, backgroundColor: colors.background }}>
<SafeAreaProvider>
Expand All @@ -38,12 +59,59 @@ export default function RootLayout() {
}}
/>
</Stack>
{pendingReport ? (
<CrashReportBanner onShare={shareReport} onDismiss={dismissReport} />
) : null}
</View>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}

function shareCrashReport(report: CrashReport) {
Share.share({ message: formatCrashReport(report) }).catch((error: unknown) => {
if (__DEV__) console.warn('[RootLayout] sharing error report failed', error);
});
}

/** Render errors anywhere in the app land here instead of closing it. */
export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
const theme = useTheme();
useEffect(() => {
recordCrash(error, 'render');
}, [error]);
const tryAgain = () => {
// Recovered in place: the app did not close, so don't offer the report
// again on next launch. "Share error details" remains available here.
takePendingCrashReport();
void retry();
};
const shareDetails = () => {
const report = takePendingCrashReport();
if (report) shareCrashReport(report);
};
return (
<View style={[styles.errorScreen, { backgroundColor: theme.colors.background }]}>
<Text style={[typography.title, styles.centered, { color: theme.colors.text }]}>
{t.crash.screenTitle}
</Text>
<Text style={[typography.callout, styles.errorBody, { color: theme.colors.textSecondary }]}>
{t.crash.screenBody}
</Text>
<Pressable
accessibilityRole="button"
onPress={tryAgain}
style={[styles.errorButton, { backgroundColor: theme.colors.accent }]}
>
<Text style={[typography.headline, styles.errorButtonLabel]}>{t.editor.tryAgain}</Text>
</Pressable>
<Pressable accessibilityRole="button" onPress={shareDetails} style={styles.errorLink}>
<Text style={[typography.subhead, { color: theme.colors.accent }]}>{t.crash.shareDetails}</Text>
</Pressable>
</View>
);
}

function useOpenInPdfImport() {
const router = useRouter();
const importingRef = useRef(false);
Expand Down Expand Up @@ -90,3 +158,19 @@ function looksLikePdfUrl(url: string): boolean {
(lower.endsWith('.pdf') || lower.includes('.pdf/'))
);
}

const styles = StyleSheet.create({
errorScreen: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: spacing.xl },
centered: { textAlign: 'center' },
errorBody: { marginTop: spacing.xs, maxWidth: 320, textAlign: 'center' },
errorButton: {
marginTop: spacing.lg,
paddingVertical: spacing.md,
paddingHorizontal: spacing.xxl,
borderRadius: radius.md,
minWidth: 220,
alignItems: 'center',
},
errorButtonLabel: { color: '#FFFFFF' },
errorLink: { marginTop: spacing.xl, paddingVertical: spacing.sm, paddingHorizontal: spacing.md },
});
2 changes: 1 addition & 1 deletion fastlane/Deliverfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ skip_screenshots true
# draft version if it does not exist yet is required - without it deliver
# loops on "Cannot find edit app store version". Keep this in sync with the
# version being prepared for release.
app_version "1.3.2"
app_version "1.3.3"

# Metadata is reviewed locally in git; precheck mostly duplicates that.
precheck_include_in_app_purchases false
Expand Down
2 changes: 1 addition & 1 deletion fastlane/metadata/de-DE/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Text erscheint jetzt an der angetippten Stelle. PDF-Exporte enthalten nun eingefügte Bilder und behalten die Seitenproportionen bei.
Importierte PDFs verschwinden nach einem App-Update nicht mehr aus Notizen. Abstürze beim Löschen einer Auswahl nach dem Widerrufen und beim PDF-Export langer Notizbücher wurden behoben. Falls OpenNotes unerwartet beendet wird, kannst du jetzt die Fehlerdetails teilen, damit wir das Problem beheben können.
2 changes: 1 addition & 1 deletion fastlane/metadata/en-US/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Fixed text appearing away from where you tap. PDF exports now include inserted images and preserve page proportions.
Fixed imported PDFs disappearing from notes after an app update. Fixed crashes when deleting a selection after Undo and when exporting long notebooks to PDF. If OpenNotes ever closes unexpectedly, you can now share the error details to help us fix it.
2 changes: 1 addition & 1 deletion fastlane/metadata/es-ES/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Corregido el texto que aparecía lejos del punto donde tocabas. Las exportaciones a PDF ahora incluyen las imágenes insertadas y conservan las proporciones de las páginas.
Los PDF importados ya no desaparecen de las notas tras actualizar la app. Se han corregido los cierres al eliminar una selección después de Deshacer y al exportar cuadernos largos a PDF. Si OpenNotes se cierra inesperadamente, ahora puedes compartir los detalles del error para ayudarnos a solucionarlo.
2 changes: 1 addition & 1 deletion fastlane/metadata/es-MX/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Corregido el texto que aparecía lejos del punto donde tocabas. Las exportaciones a PDF ahora incluyen las imágenes insertadas y conservan las proporciones de las páginas.
Los PDF importados ya no desaparecen de las notas después de actualizar la app. Se corrigieron los cierres al eliminar una selección después de Deshacer y al exportar cuadernos largos a PDF. Si OpenNotes se cierra inesperadamente, ahora puedes compartir los detalles del error para ayudarnos a solucionarlo.
2 changes: 1 addition & 1 deletion fastlane/metadata/fr-FR/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Le texte apparaît désormais à l’endroit où vous touchez la page. Les exports PDF incluent les images insérées et conservent les proportions des pages.
Les PDF importés ne disparaissent plus des notes après une mise à jour de l'app. Correction des plantages lors de la suppression d'une sélection après une annulation et de l'export PDF de longs carnets. Si OpenNotes se ferme de façon inattendue, vous pouvez désormais partager les détails de l'erreur pour nous aider à la corriger.
2 changes: 1 addition & 1 deletion fastlane/metadata/it/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Corretto il testo che appariva lontano dal punto toccato. Le esportazioni PDF ora includono le immagini inserite e mantengono le proporzioni delle pagine.
I PDF importati non scompaiono più dalle note dopo un aggiornamento dell'app. Corretti i crash durante l'eliminazione di una selezione dopo Annulla e l'esportazione in PDF di quaderni lunghi. Se OpenNotes si chiude inaspettatamente, ora puoi condividere i dettagli dell'errore per aiutarci a risolverlo.
2 changes: 1 addition & 1 deletion fastlane/metadata/ja/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
タップした位置から離れた場所にテキストが挿入される問題を修正しました。PDF書き出しに挿入画像が含まれるようになり、ページの縦横比も維持されます。
アプリのアップデート後に、読み込んだ PDF がノートから消える問題を修正しました。取り消し後に選択範囲を削除したときや、長いノートを PDF に書き出したときのクラッシュを修正しました。OpenNotes が予期せず終了した場合、エラーの詳細を共有して問題の解決に協力できるようになりました。
2 changes: 1 addition & 1 deletion fastlane/metadata/ko/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
탭한 위치와 다른 곳에 텍스트가 삽입되는 문제를 수정했습니다. 이제 PDF 내보내기에 삽입한 이미지가 포함되며 페이지 비율이 유지됩니다.
앱 업데이트 후 가져온 PDF가 노트에서 사라지는 문제를 수정했습니다. 실행 취소 후 선택 항목을 삭제할 때와 긴 노트를 PDF로 내보낼 때 발생하던 충돌을 수정했습니다. OpenNotes가 예기치 않게 종료되면 이제 오류 세부 정보를 공유해 문제 해결에 도움을 줄 수 있습니다.
2 changes: 1 addition & 1 deletion fastlane/metadata/nl-NL/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Tekst verschijnt nu op de plek waar je tikt. PDF-exports bevatten nu ingevoegde afbeeldingen en behouden de paginaverhoudingen.
Geïmporteerde pdf's verdwijnen na een app-update niet meer uit notities. Crashes bij het verwijderen van een selectie na Ongedaan maken en bij het exporteren van lange notitieboeken naar pdf zijn opgelost. Als OpenNotes onverwacht sluit, kun je nu de foutdetails delen om ons te helpen het op te lossen.
2 changes: 1 addition & 1 deletion fastlane/metadata/pt-BR/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Corrigido o texto que aparecia longe do ponto tocado. As exportações em PDF agora incluem as imagens inseridas e preservam as proporções das páginas.
PDFs importados não desaparecem mais das notas após uma atualização do app. Corrigimos falhas ao apagar uma seleção depois de Desfazer e ao exportar cadernos longos para PDF. Se o OpenNotes fechar inesperadamente, agora você pode compartilhar os detalhes do erro para nos ajudar a corrigi-lo.
2 changes: 1 addition & 1 deletion fastlane/metadata/ru/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Исправлена вставка текста вдали от места касания. При экспорте в PDF теперь сохраняются вставленные изображения и пропорции страниц.
Импортированные PDF больше не исчезают из заметок после обновления приложения. Исправлены сбои при удалении выделения после отмены и при экспорте длинных тетрадей в PDF. Если OpenNotes неожиданно закроется, теперь можно поделиться сведениями об ошибке, чтобы помочь нам её исправить.
2 changes: 1 addition & 1 deletion fastlane/metadata/zh-Hans/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
修复了文字出现在远离点击位置的问题。导出的 PDF 现在包含插入的图片,并保持页面比例。
修复了应用更新后导入的 PDF 从笔记中消失的问题。修复了撤销后删除所选内容以及将长笔记本导出为 PDF 时的崩溃。如果 OpenNotes 意外关闭,现在可以分享错误详情,帮助我们修复问题。
2 changes: 1 addition & 1 deletion fastlane/metadata/zh-Hant/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
修正了文字出現在遠離點按位置的問題。匯出的 PDF 現在包含插入的圖片,並保持頁面比例。
修正了 App 更新後匯入的 PDF 從筆記中消失的問題。修正了復原後刪除所選內容,以及將長筆記本匯出為 PDF 時的當機。如果 OpenNotes 意外關閉,現在可以分享錯誤詳細資料,協助我們修正問題。
8 changes: 4 additions & 4 deletions ios/OpenNotes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@
CODE_SIGN_ENTITLEMENTS = OpenNotes/OpenNotes.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15;
CURRENT_PROJECT_VERSION = 16;
DEVELOPMENT_TEAM = U2CPXQV7AJ;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
Expand All @@ -377,7 +377,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.2;
MARKETING_VERSION = 1.3.3;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand All @@ -403,15 +403,15 @@
CODE_SIGN_ENTITLEMENTS = OpenNotes/OpenNotes.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15;
CURRENT_PROJECT_VERSION = 16;
DEVELOPMENT_TEAM = U2CPXQV7AJ;
INFOPLIST_FILE = OpenNotes/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.2;
MARKETING_VERSION = 1.3.3;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down
Loading
Loading