diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java index 2c8c849..9a0cbd9 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java @@ -107,6 +107,7 @@ public final class SbtSettings { public static final String KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED = "drawer_date_custom_format_enabled"; public static final String KEY_DRAWER_DATE_CUSTOM_FORMAT = "drawer_date_custom_format"; + public static final String KEY_CLOCK_STORED_PATTERNS = "clock_stored_patterns"; public static final String KEY_CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS = "clock_ignore_under_display_cameras"; public static final String KEY_CLOCK_CAMERA_TYPE_OVERRIDES = "clock_camera_type_overrides"; public static final String KEY_LAYOUT_PADDING_CUTOUT_PX = "layout_padding_cutout_px"; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java index b633fe7..a4939a6 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java @@ -9,6 +9,9 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.SharedPreferences; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.net.Uri; import android.graphics.Typeface; import android.os.Bundle; import android.text.Editable; @@ -31,26 +34,44 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; public final class ClockFragment extends Fragment { + private static final String PATTERN_ORIGIN_STATUSBAR = "statusbar"; + private static final String PATTERN_ORIGIN_DRAWER_CLOCK = "drawer_clock"; + private static final String PATTERN_ORIGIN_DRAWER_DATE = "drawer_date"; + private static final int COLOR_STATUSBAR = 0xffe85050; + private static final int COLOR_DRAWER_CLOCK = 0xff4caf50; + private static final int COLOR_DRAWER_DATE = 0xff4f8cff; + private static final int CARD_STROKE_DP = 2; + private final ArrayList previewBindings = new ArrayList<>(); + private final ArrayList storedPatternBindings = new ArrayList<>(); + private final ArrayList storedPatternRowBindings = new ArrayList<>(); private final Runnable previewTicker = new Runnable() { @Override public void run() { boolean needsNextTick = false; + HashMap renderCache = new HashMap<>(); for (PreviewBinding binding : previewBindings) { needsNextTick |= updatePreview(binding); } + for (StoredPatternRowBinding binding : storedPatternRowBindings) { + needsNextTick |= updateStoredPatternPreview(binding, renderCache); + } if (needsNextTick && getView() != null) { getView().postDelayed(this, nextSecondDelayMillis()); } @@ -66,11 +87,14 @@ public final class ClockFragment extends Fragment { SharedPreferences prefs = requireContext() .getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); + MaterialCardView customFormatCard = root.findViewById(R.id.clock_custom_format_card); CheckBox customFormatEnabled = root.findViewById(R.id.clock_custom_format_enabled); View customFormatContainer = root.findViewById(R.id.clock_custom_format_container); EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input); ClockPatternPreviewView customFormatPreview = root.findViewById(R.id.clock_custom_format_preview); + MaterialCardView drawerClockCustomFormatCard = + root.findViewById(R.id.drawer_clock_custom_format_card); CheckBox drawerClockCustomFormatEnabled = root.findViewById(R.id.drawer_clock_custom_format_enabled); View drawerClockCustomFormatContainer = @@ -79,6 +103,8 @@ public final class ClockFragment extends Fragment { root.findViewById(R.id.drawer_clock_custom_format_input); ClockPatternPreviewView drawerClockCustomFormatPreview = root.findViewById(R.id.drawer_clock_custom_format_preview); + MaterialCardView drawerDateCustomFormatCard = + root.findViewById(R.id.drawer_date_custom_format_card); CheckBox drawerDateCustomFormatEnabled = root.findViewById(R.id.drawer_date_custom_format_enabled); View drawerDateCustomFormatContainer = @@ -88,6 +114,8 @@ public final class ClockFragment extends Fragment { ClockPatternPreviewView drawerDateCustomFormatPreview = root.findViewById(R.id.drawer_date_custom_format_preview); MaterialButton fontPickerButton = root.findViewById(R.id.clock_font_picker_button); + MaterialButton restoreStoredPatternsButton = + root.findViewById(R.id.clock_restore_stored_patterns_button); TextView customFormatHelpToggle = root.findViewById(R.id.clock_custom_format_help_toggle); View customFormatHelpContainer = root.findViewById(R.id.clock_custom_format_help_container); TextView customFormatHelpCommon = root.findViewById(R.id.clock_custom_format_help); @@ -115,12 +143,17 @@ public final class ClockFragment extends Fragment { SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT); previewBindings.clear(); + storedPatternBindings.clear(); + ensureDefaultStoredPatterns(prefs); bindCustomFormat( prefs, + customFormatCard, customFormatEnabled, customFormatContainer, customFormatInput, customFormatPreview, + PATTERN_ORIGIN_STATUSBAR, + COLOR_STATUSBAR, SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, customFormatEnabledValue, @@ -129,10 +162,13 @@ public final class ClockFragment extends Fragment { true); bindCustomFormat( prefs, + drawerClockCustomFormatCard, drawerClockCustomFormatEnabled, drawerClockCustomFormatContainer, drawerClockCustomFormatInput, drawerClockCustomFormatPreview, + PATTERN_ORIGIN_DRAWER_CLOCK, + COLOR_DRAWER_CLOCK, SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED, SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT, drawerClockCustomFormatEnabledValue, @@ -141,10 +177,13 @@ public final class ClockFragment extends Fragment { false); bindCustomFormat( prefs, + drawerDateCustomFormatCard, drawerDateCustomFormatEnabled, drawerDateCustomFormatContainer, drawerDateCustomFormatInput, drawerDateCustomFormatPreview, + PATTERN_ORIGIN_DRAWER_DATE, + COLOR_DRAWER_DATE, SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED, SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT, drawerDateCustomFormatEnabledValue, @@ -169,6 +208,12 @@ public final class ClockFragment extends Fragment { if (fontPickerButton != null) { fontPickerButton.setOnClickListener(v -> showFontPickerDialog()); } + if (restoreStoredPatternsButton != null) { + restoreStoredPatternsButton.setOnClickListener(v -> { + persistStoredPatterns(prefs, mergeDefaultPatterns(loadStoredPatterns(prefs))); + renderAllStoredPatternLists(prefs); + }); + } return root; } @@ -181,10 +226,13 @@ public final class ClockFragment extends Fragment { } private void bindCustomFormat(SharedPreferences prefs, + MaterialCardView card, CheckBox enabledView, View container, EditText input, ClockPatternPreviewView preview, + String origin, + int accentColor, String enabledKey, String formatKey, boolean enabled, @@ -194,6 +242,9 @@ public final class ClockFragment extends Fragment { if (enabledView == null || input == null) { return; } + styleClockCard(card, accentColor); + styleBorderedView(input, accentColor, 6, 8, 6); + styleBorderedView(preview, accentColor, 8, 10, 8); enabledView.setChecked(enabled); input.setText(format != null ? format : ""); if (preview != null) { @@ -204,6 +255,17 @@ public final class ClockFragment extends Fragment { updateCustomFormatVisibility(container, enabled); PreviewBinding binding = new PreviewBinding(enabledView, input, preview, multilinePreview); previewBindings.add(binding); + StoredPatternBinding storedBinding = createStoredPatternSection( + prefs, + container, + input, + origin, + accentColor, + multilinePreview); + if (storedBinding != null) { + storedPatternBindings.add(storedBinding); + renderStoredPatternList(prefs, storedBinding); + } updatePreview(binding); enabledView.setOnCheckedChangeListener((buttonView, isChecked) -> { updateCustomFormatVisibility(container, isChecked); @@ -233,6 +295,369 @@ public final class ClockFragment extends Fragment { }); } + private void styleClockCard(MaterialCardView card, int accentColor) { + if (card == null) { + return; + } + card.setStrokeWidth(ViewTreeSupport.dp(card.getContext(), CARD_STROKE_DP)); + card.setStrokeColor(accentColor); + } + + private void styleBorderedView(View view, + int accentColor, + int radiusDp, + int horizontalPaddingDp, + int verticalPaddingDp) { + if (view == null) { + return; + } + Context context = view.getContext(); + GradientDrawable background = new GradientDrawable(); + background.setColor(Color.TRANSPARENT); + background.setCornerRadius(ViewTreeSupport.dp(context, radiusDp)); + background.setStroke(ViewTreeSupport.dp(context, CARD_STROKE_DP), accentColor); + view.setBackground(background); + view.setPadding( + ViewTreeSupport.dp(context, horizontalPaddingDp), + ViewTreeSupport.dp(context, verticalPaddingDp), + ViewTreeSupport.dp(context, horizontalPaddingDp), + ViewTreeSupport.dp(context, verticalPaddingDp)); + } + + private StoredPatternBinding createStoredPatternSection( + SharedPreferences prefs, + View container, + EditText input, + String origin, + int accentColor, + boolean multilinePreview + ) { + if (!(container instanceof LinearLayout parent) || input == null) { + return null; + } + Context context = parent.getContext(); + MaterialButton toggle = new MaterialButton( + context, + null, + com.google.android.material.R.attr.materialButtonOutlinedStyle); + toggle.setText(R.string.clock_stored_patterns_show); + LinearLayout.LayoutParams toggleParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + toggleParams.topMargin = ViewTreeSupport.dp(context, 12); + parent.addView(toggle, toggleParams); + + LinearLayout section = new LinearLayout(context); + section.setOrientation(LinearLayout.VERTICAL); + section.setVisibility(View.GONE); + parent.addView(section, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + TextView hint = new TextView(context); + hint.setText(R.string.clock_stored_patterns_hint); + hint.setTextAppearance(android.R.style.TextAppearance_Material_Body2); + LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + hintParams.topMargin = ViewTreeSupport.dp(context, 8); + section.addView(hint, hintParams); + + LinearLayout list = new LinearLayout(context); + list.setOrientation(LinearLayout.VERTICAL); + section.addView(list, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + MaterialButton storeButton = new MaterialButton( + context, + null, + com.google.android.material.R.attr.materialButtonOutlinedStyle); + storeButton.setText(R.string.clock_store_current_pattern); + LinearLayout.LayoutParams storeParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + storeParams.topMargin = ViewTreeSupport.dp(context, 8); + section.addView(storeButton, storeParams); + + StoredPatternBinding binding = new StoredPatternBinding( + input, + origin, + accentColor, + multilinePreview, + toggle, + section, + list); + toggle.setOnClickListener(v -> { + boolean show = section.getVisibility() != View.VISIBLE; + section.setVisibility(show ? View.VISIBLE : View.GONE); + toggle.setText(show + ? R.string.clock_stored_patterns_hide + : R.string.clock_stored_patterns_show); + if (show) { + renderStoredPatternList(prefs, binding); + } else { + removeStoredPatternRowsFor(binding); + list.removeAllViews(); + schedulePreviewTickerIfNeeded(); + } + }); + storeButton.setOnClickListener(v -> { + String pattern = input.getText() != null ? input.getText().toString() : ""; + storePattern(prefs, pattern, origin); + renderAllStoredPatternLists(prefs); + }); + return binding; + } + + private void renderAllStoredPatternLists(SharedPreferences prefs) { + for (StoredPatternBinding binding : storedPatternBindings) { + renderStoredPatternList(prefs, binding); + } + } + + private void renderStoredPatternList(SharedPreferences prefs, StoredPatternBinding binding) { + if (binding == null || binding.listContainer == null) { + return; + } + Context context = binding.listContainer.getContext(); + removeStoredPatternRowsFor(binding); + binding.listContainer.removeAllViews(); + if (binding.section == null || binding.section.getVisibility() != View.VISIBLE) { + schedulePreviewTickerIfNeeded(); + return; + } + ArrayList patterns = loadStoredPatterns(prefs); + for (StoredPattern pattern : patterns) { + binding.listContainer.addView(createStoredPatternRow(context, prefs, binding, pattern)); + } + schedulePreviewTickerIfNeeded(); + } + + private View createStoredPatternRow( + Context context, + SharedPreferences prefs, + StoredPatternBinding binding, + StoredPattern pattern + ) { + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(android.view.Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + rowParams.topMargin = ViewTreeSupport.dp(context, 8); + row.setLayoutParams(rowParams); + + ClockPatternPreviewView preview = new ClockPatternPreviewView(context); + preview.setPreviewTextAppearance( + requireContext().getColor(R.color.sbt_on_surface), + previewTextSizeForOrigin(pattern.origin)); + styleBorderedView(preview, colorForOrigin(pattern.origin), 8, 10, 8); + StoredPatternRowBinding rowBinding = new StoredPatternRowBinding( + binding, + preview, + pattern.pattern, + normaliseOrigin(pattern.origin), + binding.multilinePreview); + storedPatternRowBindings.add(rowBinding); + updateStoredPatternPreview(rowBinding, new HashMap<>()); + preview.setOnClickListener(v -> { + binding.input.setText(pattern.pattern); + binding.input.setSelection(binding.input.getText() != null + ? binding.input.getText().length() + : 0); + }); + row.addView(preview, new LinearLayout.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f)); + + MaterialButton deleteButton = new MaterialButton(context); + deleteButton.setText("×"); + deleteButton.setTextColor(Color.WHITE); + deleteButton.setBackgroundTintList(android.content.res.ColorStateList.valueOf( + ContextCompat.getColor(context, R.color.sbt_danger))); + deleteButton.setMinWidth(0); + deleteButton.setMinHeight(0); + deleteButton.setPadding(0, 0, 0, 0); + deleteButton.setOnClickListener(v -> { + deleteStoredPattern(prefs, pattern.pattern); + renderAllStoredPatternLists(prefs); + }); + LinearLayout.LayoutParams deleteParams = new LinearLayout.LayoutParams( + ViewTreeSupport.dp(context, 44), + ViewTreeSupport.dp(context, 44)); + deleteParams.leftMargin = ViewTreeSupport.dp(context, 8); + row.addView(deleteButton, deleteParams); + return row; + } + + private void removeStoredPatternRowsFor(StoredPatternBinding binding) { + for (int i = storedPatternRowBindings.size() - 1; i >= 0; i--) { + if (storedPatternRowBindings.get(i).owner == binding) { + storedPatternRowBindings.remove(i); + } + } + } + + private void ensureDefaultStoredPatterns(SharedPreferences prefs) { + if (prefs == null || prefs.contains(SbtSettings.KEY_CLOCK_STORED_PATTERNS)) { + return; + } + persistStoredPatterns(prefs, defaultStoredPatterns()); + } + + private ArrayList mergeDefaultPatterns(ArrayList current) { + ArrayList result = current != null ? current : new ArrayList<>(); + for (StoredPattern pattern : defaultStoredPatterns()) { + upsertPattern(result, pattern.pattern, pattern.origin); + } + return result; + } + + private ArrayList defaultStoredPatterns() { + ArrayList patterns = new ArrayList<>(); + patterns.add(new StoredPattern("H:mm:ss", PATTERN_ORIGIN_STATUSBAR)); + patterns.add(new StoredPattern("{big}H:mm:{/big color(#batterybar)}ss", PATTERN_ORIGIN_STATUSBAR)); + patterns.add(new StoredPattern( + "{@sans-serif-condensed-light color(#fff;#000)}|d{\\n30 big}H:mm:|{/big color(#batterybar invRGB 0.5 *RGB invRGB;#batterybar 0.5 *RGB)}ss", + PATTERN_ORIGIN_STATUSBAR)); + patterns.add(new StoredPattern("{@sans-serif-condensed-light}H:mm:{small}ss", PATTERN_ORIGIN_DRAWER_CLOCK)); + patterns.add(new StoredPattern("EEE, d MMM", PATTERN_ORIGIN_DRAWER_DATE)); + patterns.add(new StoredPattern("EEE, MMM d", PATTERN_ORIGIN_DRAWER_DATE)); + patterns.add(new StoredPattern("{@sans-serif-condensed-light}EEEE, MMMM d", PATTERN_ORIGIN_DRAWER_DATE)); + patterns.add(new StoredPattern("{@sans-serif-condensed-light}EEEE, d MMMM", PATTERN_ORIGIN_DRAWER_DATE)); + return patterns; + } + + private void storePattern(SharedPreferences prefs, String pattern, String origin) { + String value = pattern != null ? pattern.trim() : ""; + if (value.isEmpty()) { + return; + } + ArrayList patterns = loadStoredPatterns(prefs); + upsertPattern(patterns, value, origin); + persistStoredPatterns(prefs, patterns); + } + + private void upsertPattern(ArrayList patterns, String pattern, String origin) { + if (patterns == null || pattern == null || pattern.trim().isEmpty()) { + return; + } + String value = pattern.trim(); + String resolvedOrigin = normaliseOrigin(origin); + for (int i = 0; i < patterns.size(); i++) { + StoredPattern existing = patterns.get(i); + if (existing != null && value.equals(existing.pattern)) { + patterns.set(i, new StoredPattern(value, resolvedOrigin)); + return; + } + } + patterns.add(new StoredPattern(value, resolvedOrigin)); + } + + private void deleteStoredPattern(SharedPreferences prefs, String pattern) { + ArrayList patterns = loadStoredPatterns(prefs); + for (int i = patterns.size() - 1; i >= 0; i--) { + StoredPattern existing = patterns.get(i); + if (existing != null && existing.pattern.equals(pattern)) { + patterns.remove(i); + } + } + persistStoredPatterns(prefs, patterns); + } + + private ArrayList loadStoredPatterns(SharedPreferences prefs) { + ArrayList patterns = new ArrayList<>(); + if (prefs == null) { + return patterns; + } + String encoded = prefs.getString(SbtSettings.KEY_CLOCK_STORED_PATTERNS, ""); + if (encoded == null || encoded.isEmpty()) { + return patterns; + } + String[] lines = encoded.split("\\n", -1); + for (String line : lines) { + StoredPattern pattern = decodeStoredPattern(line); + if (pattern == null) { + continue; + } + upsertPattern(patterns, pattern.pattern, pattern.origin); + } + return patterns; + } + + private void persistStoredPatterns(SharedPreferences prefs, ArrayList patterns) { + if (prefs == null) { + return; + } + StringBuilder encoded = new StringBuilder(); + if (patterns != null) { + for (StoredPattern pattern : patterns) { + if (pattern == null || pattern.pattern == null || pattern.pattern.trim().isEmpty()) { + continue; + } + if (encoded.length() > 0) { + encoded.append('\n'); + } + encoded.append(normaliseOrigin(pattern.origin)) + .append('|') + .append(Uri.encode(pattern.pattern)); + } + } + prefs.edit().putString(SbtSettings.KEY_CLOCK_STORED_PATTERNS, encoded.toString()).apply(); + } + + private StoredPattern decodeStoredPattern(String line) { + if (line == null || line.isEmpty()) { + return null; + } + int split = line.indexOf('|'); + if (split <= 0 || split >= line.length() - 1) { + return null; + } + String origin = normaliseOrigin(line.substring(0, split)); + String pattern = Uri.decode(line.substring(split + 1)); + if (pattern == null || pattern.trim().isEmpty()) { + return null; + } + return new StoredPattern(pattern.trim(), origin); + } + + private String normaliseOrigin(String origin) { + if (PATTERN_ORIGIN_DRAWER_CLOCK.equals(origin) + || PATTERN_ORIGIN_DRAWER_DATE.equals(origin) + || PATTERN_ORIGIN_STATUSBAR.equals(origin)) { + return origin; + } + return PATTERN_ORIGIN_STATUSBAR; + } + + private int colorForOrigin(String origin) { + switch (normaliseOrigin(origin)) { + case PATTERN_ORIGIN_DRAWER_CLOCK: + return COLOR_DRAWER_CLOCK; + case PATTERN_ORIGIN_DRAWER_DATE: + return COLOR_DRAWER_DATE; + case PATTERN_ORIGIN_STATUSBAR: + default: + return COLOR_STATUSBAR; + } + } + + private float previewTextSizeForOrigin(String origin) { + switch (normaliseOrigin(origin)) { + case PATTERN_ORIGIN_DRAWER_CLOCK: + return 24f; + case PATTERN_ORIGIN_DRAWER_DATE: + return 16f; + case PATTERN_ORIGIN_STATUSBAR: + default: + return 18f; + } + } + private boolean updatePreview(PreviewBinding binding) { if (binding == null || binding.preview == null) { return false; @@ -260,6 +685,36 @@ public final class ClockFragment extends Fragment { return result.containsSeconds(); } + private boolean updateStoredPatternPreview( + StoredPatternRowBinding binding, + Map cache + ) { + if (binding == null + || binding.preview == null + || binding.owner == null + || binding.owner.section == null + || binding.owner.section.getVisibility() != View.VISIBLE) { + return false; + } + String key = binding.origin + "\u0000" + binding.multilinePreview + "\u0000" + binding.pattern; + ClockPatternPreviewRenderer.Result result = cache != null ? cache.get(key) : null; + if (result == null) { + result = ClockPatternPreviewRenderer.render( + binding.preview.getContext(), + binding.pattern, + binding.preview.getPreviewTextColor()); + if (!binding.multilinePreview) { + result = flattenRows(result); + } + if (cache != null) { + cache.put(key, result); + } + } + binding.preview.setPreview(result); + binding.preview.setContentDescription(result.contentDescription()); + return result.containsSeconds(); + } + private ClockPatternPreviewRenderer.Result flattenRows(ClockPatternPreviewRenderer.Result result) { if (result == null || result.rows() == null || result.rows().size() <= 1) { return result; @@ -324,6 +779,8 @@ public final class ClockFragment extends Fragment { view.removeCallbacks(previewTicker); } previewBindings.clear(); + storedPatternBindings.clear(); + storedPatternRowBindings.clear(); super.onDestroyView(); } @@ -478,6 +935,29 @@ public final class ClockFragment extends Fragment { private record PreviewBinding(CheckBox enabled, EditText input, ClockPatternPreviewView preview, boolean multilinePreview) { } + private record StoredPatternBinding( + EditText input, + String origin, + int accentColor, + boolean multilinePreview, + MaterialButton toggle, + View section, + LinearLayout listContainer + ) { + } + + private record StoredPatternRowBinding( + StoredPatternBinding owner, + ClockPatternPreviewView preview, + String pattern, + String origin, + boolean multilinePreview + ) { + } + + private record StoredPattern(String pattern, String origin) { + } + private static final class FontFamilyAdapter extends BaseAdapter { private static final String SAMPLE_PATTERN = "EEEE HH:mm:ss"; diff --git a/app/src/main/res/layout/fragment_clock.xml b/app/src/main/res/layout/fragment_clock.xml index 25fd0f7..dfdb1e3 100644 --- a/app/src/main/res/layout/fragment_clock.xml +++ b/app/src/main/res/layout/fragment_clock.xml @@ -18,6 +18,7 @@ android:textAppearance="?attr/textAppearanceHeadline5" /> + + Shorthand blocks:\n\u2022 {#FA0 big @sans-serif-condensed} is shorthand for opening multiple tags in order\n\u2022 {#FA0} is shorthand for {color(#FA0)}\n\u2022 In shorthand: / closes a tag, color(...) sets font colour, @ sets font face\n\u2022 Each whitespace-separated token inside {} becomes its own tag, except color(...) which may contain spaces\n\nLine breaks:\n\u2022 Use \\n, {\\n} or {\\n15}; the number is the line-height delta in statusbar units\n\u2022 Omit the number to use the default row spacing\n\u2022 { /\\n } style line breaks are written without spaces: {/\\n} or {/\\n15}\n\u2022 Normal line breaks propagate open tags onto the next row\n\u2022 A / line break ends all currently open tags before the next row starts\n\nPipes and grouping:\n\u2022 Maximum two pipes per row; more than two is a syntax error for that row only\n\u2022 Pipes inside single quotes or {} do not split the row\n\u2022 One pipe splits into left and right segments\n\u2022 Two pipes remove the middle section, then split using the outer pipes\n\u2022 Tags are logically continued across pipe splits, even if the middle section is ignored\n\u2022 Rows without pipes are independent; rows with pipes share one common pipe group\n\u2022 A syntax-error row becomes the text Syntax error, with no whole-clock fallback\n\nColours:\n\u2022 Supported operands: #RGB, #ARGB, #RRGGBB, #AARRGGBB, #batterybar, [R,G,B], [A,R,G,B], and numbers\n\u2022 Colour expressions use reverse Polish notation: operands first, operator last\n\u2022 Use ; to separate bright/dark variants; the dark side starts with the raw result from the bright side on the stack\n\u2022 Operators: + - * / ^ inv min max sqrt clip dup\n\u2022 Operators can be channel-filtered, for example +A, *RGB, invRGB, maxRGB, clipA\n\u2022 Final values are clipped to 0..255; a final number becomes opaque grayscale Examples:\n\u2022 <small>EEE d MMM</small> HH:mm\n\u2022 HH<small>:mm</small>\n\u2022 <font color=\'#FA0\'>ss</font>\n\u2022 {color(#FA0 ; invRGB 0.8 *RGB)}ss\n\u2022 {color(#batterybar 1.2 * ; invRGB 0.5 *RGB) big u @sans-serif-condensed}HH:mm\n\u2022 {color([255, 255, 170, 0] ; invRGB)}HH:mm\n\u2022 HH:mm{\\n12 small}EEE d MMM\n\u2022 HH|mm aligns the pipe position across every piped row\n\u2022 left||right removes the middle section before split alignment\n\u2022 {/font}{/\\n big}HH:mm starts a new row without carrying the previous font tags forward Browse font families + Restore stored patterns + Show stored patterns + Hide stored patterns + Tap a preview to use it + Store current pattern ▾ Show pattern instructions ▴ Hide pattern instructions Font families (tap to copy)