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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ takes 500 characters, so not everything here reaches the store.

## Unreleased

- Editing reaches PowerPoint files, spreadsheet cells and plain text files, has undo and
redo, and no longer loads the document again, so the page stays where it was.
- Pro also formats text, starts and joins paragraphs, and marks up PDFs: highlight,
underline, strike out, squiggly underline, and drawing.
- Leaving the edit mode with unsaved changes asks whether to save or discard them.
- Word files show a text's shading, and text that is both underlined and struck through
shows both lines.
- A spreadsheet too big to show in full says so, and names how many of its rows
and columns are on screen. It used to stop without a word.
- How much of a sheet is shown follows the device's memory now, rather than one
Expand Down
46 changes: 34 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,20 @@ and `src/review`, with a no-op of the same shape in `src/noAds` and `src/noRevie
nothing proprietary and stays in `src/main`. A method added to one copy has to be added
to the other, which `assembleDebug` catches - it builds all three.

Code that has to *ask* reads `Features`, never the flavor name: `Features.withAds` is the
one question anything asks today, and `LINKS_ADS` behind it sits in `src/ads` and
`src/noAds` next to the classes it stands for, so the flag cannot end up in a build whose
code says otherwise. Do not add a `BuildConfig.FLAVOR` comparison back - it was what made
`BillingManager` miss foss - and do not name a flag after a behaviour it only implies. The
resource bool `DISABLE_TRACKING` was both mistakes at once: there is no tracking to
disable, `AnalyticsManager` and `CrashManager` write to logcat and nowhere else.
Code that has to *ask* reads `Features`, never the flavor name. `Features.withAds` comes
from `LINKS_ADS`, which sits in `src/ads` and `src/noAds` next to the classes it stands for,
so the flag cannot end up in a build whose code says otherwise. `Features.advancedEditing`,
from `ADVANCED_EDITING` in the same two files, is what pro is sold on: new and joined
paragraphs, formatting, and marks on a pdf. Every other edit the core takes - inside one
paragraph, a sheet cell, a plain text file - is in every build. `Features.offersEditing` is the
one list of it; the Edit button still stands on the core's answer, so over a pdf in lite it
offers pro instead. `OpenDocument.ios` draws the same line with the same flag, beside its own
`LINKS_ADS`.

Do not add a `BuildConfig.FLAVOR` comparison back - it was what made `BillingManager` miss
foss - and do not name a flag after a behaviour it only implies. The resource bool
`DISABLE_TRACKING` was both mistakes at once: there is no tracking to disable,
`AnalyticsManager` and `CrashManager` write to logcat and nowhere else.

Those two take no switch at all, which is why `DocumentLoader` just constructs them. Ads
and billing are what `MainActivity.initializeManagers` gates, on `Features.withAds` *and*
Expand Down Expand Up @@ -296,19 +303,34 @@ deck opened in portrait keeps a portrait-sized slide in a landscape screen. `ini

### Editability comes from the core, never from a mime type

`Document.isEditable()`/`isSavable()` decides whether `DocumentFragment` offers the Edit
button, carried on `LoadedDocument.isEditable`. `CoreLoader.host()` only holds a document
open when the core says yes, so having one *is* the answer. Do not reintroduce a list of
editable formats in the UI.
`CoreLoader.editingOf` asks the opened file what the user can change, and the answer rides on
`LoadedDocument.editing` as an `EditingKind`: `DOCUMENT` for a text document or a
presentation, `SHEET`, `TEXT` for a plain file, `ANNOTATION` for a pdf, `NONE`. It is the
file's own answer - `Document.isEditable()`/`isSavable()`, `TextFile.isSavable()`,
`PdfFile.isAnnotatable()` - so a decrypted document or a repaired pdf says no. Do not
reintroduce a list of editable formats in the UI.

`DecodedFile.capabilities()` is asked first, as a shortcut: opening a document costs a
second parse, so a format declaring no `edit`/`save` is never opened to be told no. It is an
upper bound - the document still answers.
upper bound - the file still answers.

Decryption is the same shape. `capabilities().decrypt` says whether a password is worth asking
for, and `CoreLoader.host` refuses an encrypted `.doc`, `.ppt` or `.xls` on it rather than
raising a dialog no password can close. The app must not learn that list for itself.

**The editor is in the page, and it is always there.** A document the core can write back is
rendered with `HtmlConfig.editable`, and the edit button only calls `odr.editing.enable()` -
no second render, so the reader stays where they were. The page owns the operation log, undo
and the refusals; `editing-bridge.js` (injected by `PageView` on every page load) forwards its
callbacks. Lite narrows `HtmlConfig.editingScope` to `PARAGRAPH`, and the page refuses the
rest with `outOfScope`, which `DocumentFragment` answers with the offer of pro, once an edit.
A pdf needs no scaffolding: every pdf page carries `odr.annotation`.

**Nothing is held open between the render and the save.** `CoreLoader.writeEdits` opens the
cached copy again and applies the page's payload with the call its kind takes -
`Document.edit` and `save`, `TextFile.edit` and `save`, `PdfFile.annotate`. An edit that throws
halfway leaves the document it was applied to half changed, so a retry must not start from it.

### Storage access

The app declares **no storage permission**, only `INTERNET`, and has to stay that way:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ a sideload carrying an older one neither updates nor complains: install the new
uninstall the old one. Nothing carries over - a recent documents list whose uri permissions
die with the old package anyway.

## Editions

Play carries two apps: OpenDocument Reader, free with ads, and OpenDocument Reader Pro,
paid. Both open everything, and both edit: the text of a document inside one paragraph, the
cells of a spreadsheet, and plain text files. Pro also starts and joins paragraphs, formats
text, and marks up PDFs.

The F-Droid build and the apk on the release page are Pro without Play's review sheet: no
ads, and every edit.

## Translations

The app speaks nineteen languages and the Play listing fifteen, and both are written
Expand Down
3 changes: 3 additions & 0 deletions app/src/ads/java/app/opendocument/droid/nonfree/Linked.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ package app.opendocument.droid.nonfree

/** Read through [Features]. */
internal const val LINKS_ADS = true

/** Read through [Features]. */
internal const val ADVANCED_EDITING = false
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class DocumentParcelTest {
),
// the middle sheet is the only one the budget cut
listOf(null, SheetCut(80000, 12, 8333, 12), null),
isEditable = true,
editing = EditingKind.DOCUMENT,
readsAsDocument = true,
)

Expand All @@ -116,7 +116,7 @@ class DocumentParcelTest {
assertEquals("budget.ods", restored.file.filename)
assertEquals(listOf("hey", "ho", "Sheet3"), restored.partTitles)
assertEquals(document.partUris, restored.partUris)
assertTrue(restored.isEditable)
assertEquals(EditingKind.DOCUMENT, restored.editing)
assertTrue(restored.readsAsDocument)

assertNull(restored.partCuts[0])
Expand Down Expand Up @@ -147,7 +147,7 @@ class DocumentParcelTest {
listOf<String?>(null),
listOf(Uri.parse("http://localhost:29665/file/odr/document.html")),
listOf(null),
isEditable = false,
editing = EditingKind.NONE,
readsAsDocument = true,
),
LoadedDocument.CREATOR,
Expand All @@ -156,7 +156,7 @@ class DocumentParcelTest {
assertEquals(1, restored.partTitles.size)
assertNull(restored.partTitles[0])
assertNull(restored.partCuts[0])
assertEquals(false, restored.isEditable)
assertEquals(EditingKind.NONE, restored.editing)
assertTrue(restored.readsAsDocument)
}

Expand Down
Loading
Loading