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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public final class SongbirdPrefsHelper {
private static final String KEY_BASE_URL = "songbird_base_url";
private static final String KEY_USERNAME = "songbird_username";
private static final String KEY_PASSWORD = "songbird_password";
private static final String KEY_DEFAULT_TAGS = "songbird_default_tags";

/** Out-of-box default tag (preserves the documented {@code ["sermon"]} import default). */
private static final String DEFAULT_TAGS_FALLBACK = "sermon";

private SongbirdPrefsHelper() {}

Expand Down Expand Up @@ -64,6 +68,21 @@ public static void setPassword(Context ctx, String value) {
if (p != null) p.edit().putString(KEY_PASSWORD, value == null ? "" : value).apply();
}

/**
* The operator's default tags (comma-separated) to prefill the edit screen. Defaults to {@code
* "sermon"} until the operator sets (or explicitly blanks) it in Settings.
*/
public static String getDefaultTags(Context ctx) {
SharedPreferences p = open(ctx);
return p == null ? DEFAULT_TAGS_FALLBACK : p.getString(KEY_DEFAULT_TAGS, DEFAULT_TAGS_FALLBACK);
}

/** Persists the default tags (trimmed). An explicit blank is honored (empty tag box). */
public static void setDefaultTags(Context ctx, String value) {
SharedPreferences p = open(ctx);
if (p != null) p.edit().putString(KEY_DEFAULT_TAGS, value == null ? "" : value.trim()).apply();
}

public static boolean isConfigured(Context ctx) {
return SongbirdSettings.canSend(getBaseUrl(ctx), getUsername(ctx), getPassword(ctx));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import de.schliweb.makeacopy.R;
import de.schliweb.makeacopy.songbird.SongbirdPrefsHelper;
import de.schliweb.makeacopy.anchor.BookNames;
import de.schliweb.makeacopy.anchor.SpanResolution;
import de.schliweb.makeacopy.anchor.VerseTable;
Expand Down Expand Up @@ -75,14 +77,26 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
android.widget.Toast.LENGTH_LONG);
}
String combined = CombinedOcrTextProvider.fromPages(session.getPages().getValue());
viewModel.initialize(combined, table, LocalDate.now(ZoneId.systemDefault()).toString());
viewModel.initialize(
combined,
table,
LocalDate.now(ZoneId.systemDefault()).toString(),
SongbirdPrefsHelper.getDefaultTags(requireContext()));

setupBookPicker();
bindInitialFields();
wireWatchers();
wireDatePicker();
wireContinue();

// Keep the Continue button clear of the system nav bar (edge-to-edge).
ViewCompat.setOnApplyWindowInsetsListener(
binding.buttonContinue,
(v, insets) -> {
UIUtils.adjustMarginForSystemInsets(binding.buttonContinue, 16);
return insets;
});

viewModel.getState().observe(getViewLifecycleOwner(), this::renderDerived);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,18 @@ public LiveData<EditUiState> getState() {
* @param combinedText the F2 combined OCR string
* @param verseTable the bundled verse-count table (injected; loaded by the fragment)
* @param todayIso today's date as {@code yyyy-MM-dd}
* @param defaultTags the operator's configured default tags (comma-separated) to prefill the tag
* box; passed verbatim (the {@code "sermon"} fallback lives in the prefs helper)
*/
public void initialize(String combinedText, VerseTable verseTable, String todayIso) {
public void initialize(
String combinedText, VerseTable verseTable, String todayIso, String defaultTags) {
if (initialized) return;
initialized = true;
this.table = verseTable;
this.editedText = combinedText == null ? "" : combinedText;
this.title = "";
this.dateIso = todayIso == null ? "" : todayIso;
this.tagsText = "sermon";
this.tagsText = defaultTags == null ? "" : defaultTags;

Optional<StructuralAnchor> found = AnchorFinder.find(this.editedText);
if (found.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ public LiveData<SermonDraft> getDraft() {
public void setDraft(SermonDraft d) {
draft.setValue(d);
}

/** Clears the handed-off draft (e.g. after a successful send, before returning to start). */
public void clear() {
draft.setValue(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,14 @@ public void handleOnBackPressed() {
return insets;
});

// Keep the bottom-anchored Continue container clear of the system nav bar (edge-to-edge).
ViewCompat.setOnApplyWindowInsetsListener(
binding.exportOptionsGroup,
(v, insets) -> {
UIUtils.adjustMarginForSystemInsets(binding.exportOptionsGroup, 8);
return insets;
});

exportViewModel
.isDocumentReady()
.observe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavOptions;
import androidx.navigation.Navigation;
import de.schliweb.makeacopy.BuildConfig;
import de.schliweb.makeacopy.R;
Expand All @@ -30,7 +31,10 @@
import de.schliweb.makeacopy.songbird.ImportResult;
import de.schliweb.makeacopy.songbird.ShareFilename;
import de.schliweb.makeacopy.songbird.SongbirdPrefsHelper;
import de.schliweb.makeacopy.ui.camera.CameraViewModel;
import de.schliweb.makeacopy.ui.crop.CropViewModel;
import de.schliweb.makeacopy.ui.edit.SermonDraftViewModel;
import de.schliweb.makeacopy.ui.export.session.ExportSessionViewModel;
import de.schliweb.makeacopy.ui.finalize.FinalizeViewModel.Phase;
import de.schliweb.makeacopy.utils.ui.UIUtils;
import java.io.File;
Expand All @@ -45,10 +49,14 @@
@dagger.hilt.android.AndroidEntryPoint
public class FinalizeFragment extends Fragment {

/** Brief pause so the operator sees the created/skipped result before the screen returns to start. */
private static final long AUTO_RETURN_DELAY_MS = 1500L;

private FragmentFinalizeBinding binding;
private FinalizeViewModel viewModel;
private SermonDraft draft;
private String json;
private boolean returning; // one-shot guard: a successful send auto-returns exactly once

@Override
public View onCreateView(
Expand Down Expand Up @@ -138,10 +146,47 @@ private void renderState(FinalizeViewModel.SendUiState state) {
binding.resultText.setText(R.string.finalize_sending);
} else if (state.phase() == Phase.DONE && state.result() != null) {
binding.resultText.setText(describe(state.result()));
maybeAutoReturn(state.result());
}
updateSendGate();
}

/**
* A clean import (SUCCESS, no rejected entries) ends the workflow: briefly show the result, then
* clear the session and return to the camera start screen so the next handout starts fresh. This
* also prevents re-tapping Send on a note that already imported. Failures stay put so the operator
* can read the error and retry.
*/
private void maybeAutoReturn(ImportResult r) {
if (returning || r == null || !r.isSuccess() || r.failed() > 0) return;
returning = true;
binding.buttonSend.setEnabled(false);
binding.getRoot().postDelayed(this::returnToStart, AUTO_RETURN_DELAY_MS);
}

/** Mirrors the page hub's "start over" reset (ExportFragment): wipe session + capture state. */
private void returnToStart() {
if (binding == null || !isAdded()) return;
ViewModelProvider activity = new ViewModelProvider(requireActivity());
activity.get(ExportSessionViewModel.class).setInitial(null);
activity.get(SermonDraftViewModel.class).clear();
CameraViewModel cameraViewModel = activity.get(CameraViewModel.class);
CropViewModel cropViewModel = activity.get(CropViewModel.class);
cameraViewModel.setImageUri(null);
cropViewModel.setImageCropped(false);
cropViewModel.setImageBitmap(null);
cropViewModel.setOriginalImageBitmap(null);
cropViewModel.setImageLoaded(false);
NavOptions navOptions =
new NavOptions.Builder().setPopUpTo(R.id.navigation_camera, true).build();
try {
Navigation.findNavController(requireView())
.navigate(R.id.navigation_camera, null, navOptions);
} catch (IllegalArgumentException | IllegalStateException ignored) {
// destination unavailable — no-op
}
}

private String describe(ImportResult r) {
switch (r.status()) {
case SUCCESS:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import de.schliweb.makeacopy.R;
Expand Down Expand Up @@ -47,7 +48,16 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
binding.baseUrlField.setText(SongbirdPrefsHelper.getBaseUrl(requireContext()));
binding.usernameField.setText(SongbirdPrefsHelper.getUsername(requireContext()));
binding.passwordField.setText(SongbirdPrefsHelper.getPassword(requireContext()));
binding.defaultTagsField.setText(SongbirdPrefsHelper.getDefaultTags(requireContext()));
binding.buttonSaveSettings.setOnClickListener(v -> save());

// Keep the Save button clear of the system nav bar (edge-to-edge).
ViewCompat.setOnApplyWindowInsetsListener(
binding.buttonSaveSettings,
(v, insets) -> {
UIUtils.adjustMarginForSystemInsets(binding.buttonSaveSettings, 16);
return insets;
});
}

private void save() {
Expand All @@ -57,9 +67,14 @@ private void save() {
binding.usernameField.getText() == null ? "" : binding.usernameField.getText().toString();
String password =
binding.passwordField.getText() == null ? "" : binding.passwordField.getText().toString();
String defaultTags =
binding.defaultTagsField.getText() == null
? ""
: binding.defaultTagsField.getText().toString();
SongbirdPrefsHelper.setBaseUrl(requireContext(), baseUrl); // normalized inside
SongbirdPrefsHelper.setUsername(requireContext(), username);
SongbirdPrefsHelper.setPassword(requireContext(), password);
SongbirdPrefsHelper.setDefaultTags(requireContext(), defaultTags);
UIUtils.showToast(requireContext(), getString(R.string.settings_saved), Toast.LENGTH_SHORT);
try {
Navigation.findNavController(requireView()).popBackStack();
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/res/layout/fragment_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@
android:importantForAutofill="no" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/default_tags_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/settings_default_tags_hint">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/default_tags_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:importantForAutofill="no" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.button.MaterialButton
android:id="@+id/button_save_settings"
android:layout_width="match_parent"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -422,5 +422,6 @@
<string name="settings_base_url_hint" tools:ignore="MissingTranslation">Base URL (e.g. http://host:8000)</string>
<string name="settings_username_hint" tools:ignore="MissingTranslation">Username</string>
<string name="settings_password_hint" tools:ignore="MissingTranslation">Password</string>
<string name="settings_default_tags_hint" tools:ignore="MissingTranslation">Default tags (comma-separated)</string>
<string name="settings_saved" tools:ignore="MissingTranslation">Settings saved</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private EditUiState s() {

@Test
public void initialize_prefillsTextAndAnchorFromFinder() {
vm.initialize("The army gathered. (1Sam. 22:1)", table, "2026-06-10");
vm.initialize("The army gathered. (1Sam. 22:1)", table, "2026-06-10", "sermon");
EditUiState st = s();
assertEquals("The army gathered. (1Sam. 22:1)", st.editedText());
assertEquals("1SA", st.bookUsfm());
Expand All @@ -92,9 +92,25 @@ public void initialize_prefillsTextAndAnchorFromFinder() {
assertEquals(SpanResolution.Status.RESOLVED, st.anchorStatus());
}

@Test
public void initialize_prefillsConfiguredDefaultTags() {
vm.initialize("", table, "2026-06-10", "sermon, majestic view");
EditUiState st = s();
assertEquals("sermon, majestic view", st.tagsText());
assertEquals(Arrays.asList("sermon", "majestic view"), st.tags());
}

@Test
public void initialize_blankDefaultTags_yieldsNoTags() {
vm.initialize("", table, "2026-06-10", "");
EditUiState st = s();
assertEquals("", st.tagsText());
assertTrue(st.tags().isEmpty());
}

@Test
public void initialize_noAnchor_leavesFieldsEmptyAndBlocks() {
vm.initialize("No scripture references here at all.", table, "2026-06-10");
vm.initialize("No scripture references here at all.", table, "2026-06-10", "sermon");
EditUiState st = s();
assertEquals("", st.bookUsfm());
assertEquals("", st.chapterText());
Expand All @@ -104,17 +120,17 @@ public void initialize_noAnchor_leavesFieldsEmptyAndBlocks() {

@Test
public void initialize_isGuarded_secondCallDoesNotWipeEdits() {
vm.initialize("(1Sam. 22:1)", table, "2026-06-10");
vm.initialize("(1Sam. 22:1)", table, "2026-06-10", "sermon");
vm.setChapter("5");
vm.initialize("Totally different text", table, "2026-06-11");
vm.initialize("Totally different text", table, "2026-06-11", "sermon");
EditUiState st = s();
assertEquals("5", st.chapterText());
assertEquals("1SA", st.bookUsfm());
}

@Test
public void chapterOnly_resolvesWholeChapter() {
vm.initialize("", table, "2026-06-10");
vm.initialize("", table, "2026-06-10", "sermon");
vm.setBookUsfm("PSA");
vm.setChapter("23");
EditUiState st = s();
Expand All @@ -126,7 +142,7 @@ public void chapterOnly_resolvesWholeChapter() {

@Test
public void chapterOutOfRange_blocks() {
vm.initialize("", table, "2026-06-10");
vm.initialize("", table, "2026-06-10", "sermon");
vm.setBookUsfm("MAT"); // sample MAT has 5 chapters
vm.setChapter("9");
EditUiState st = s();
Expand All @@ -136,7 +152,7 @@ public void chapterOutOfRange_blocks() {

@Test
public void incompleteAnchor_blocks() {
vm.initialize("", table, "2026-06-10");
vm.initialize("", table, "2026-06-10", "sermon");
vm.setBookUsfm("PSA");
vm.setChapter(""); // no chapter yet
EditUiState st = s();
Expand All @@ -146,7 +162,7 @@ public void incompleteAnchor_blocks() {

@Test
public void reversedVerseRange_warnsButDoesNotBlock() {
vm.initialize("", table, "2026-06-10");
vm.initialize("", table, "2026-06-10", "sermon");
vm.setBookUsfm("1SA");
vm.setChapter("25");
vm.setVerseFrom("6");
Expand All @@ -160,7 +176,7 @@ public void reversedVerseRange_warnsButDoesNotBlock() {

@Test
public void setText_doesNotMoveTheAnchor() {
vm.initialize("(1Sam. 22:1)", table, "2026-06-10");
vm.initialize("(1Sam. 22:1)", table, "2026-06-10", "sermon");
vm.setText("Now it says John 1:1 instead");
EditUiState st = s();
assertEquals("1SA", st.bookUsfm());
Expand All @@ -170,15 +186,15 @@ public void setText_doesNotMoveTheAnchor() {

@Test
public void titleWarning_trueWhenBlank_falseWhenSet() {
vm.initialize("", table, "2026-06-10");
vm.initialize("", table, "2026-06-10", "sermon");
assertTrue(s().titleWarning());
vm.setTitle("A Story about David & Abigail");
assertFalse(s().titleWarning());
}

@Test
public void buildDraft_assemblesAllInputs() {
vm.initialize("edited body text", table, "2026-06-10");
vm.initialize("edited body text", table, "2026-06-10", "sermon");
vm.setBookUsfm("1SA");
vm.setChapter("25");
vm.setVerseFrom("3");
Expand All @@ -198,7 +214,7 @@ public void buildDraft_assemblesAllInputs() {

@Test
public void buildDraft_nullWhenCannotProceed() {
vm.initialize("", table, "2026-06-10");
vm.initialize("", table, "2026-06-10", "sermon");
assertNull(vm.buildDraft());
}

Expand Down
Loading