Builds as one slide per step when the template has Build 1, 2, ... (v1.3.2)
- Template.step_builds: build slides without ProPresenter's build feature;
each build in the notes becomes one slide per step from the template's
k-th build slide (last one reused): heading + first item, then one more
line per slide; font runs cut to each step's text
- Build headings (":"/"?"/italic, or above a numbered / same-word list) kept
as Line.lead; TextEdit format marks them with ">"
- Prefer a one-slide-per-step template over a newer old-style one
- Clear message when a notes file in Google Drive can't be read
- README: builds, underline, fonts are never changed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,12 +75,23 @@ Tools first if Python is missing). The first run asks which campus it is and sav
|
||||
|---|---|
|
||||
| first line / second line / bare reference | **Title** slide (title, subtitle, main passage) |
|
||||
| paragraph starting with a reference (`John 1:12 ESV …`) | **SCRIPTURE**. Bold words stay bold. |
|
||||
| underlined words (any slide) | underlined on the slide |
|
||||
| a line on its own | **Point** |
|
||||
| lead-in (`…:` / `…?` / italic) + one line | one **Point**, lead-in in the italic style |
|
||||
| 3+ lines together, a list, or after "(one at a time)" | **\*STREAM DECK\*** slide + **Build** (one step per line) |
|
||||
| 3+ lines together, a list, or after "(one at a time)" | a **Build**: one step per line (see below) |
|
||||
| `Sermon in a Sentence:` + sentence | **SERMON IN A SENTENCE** |
|
||||
| `(…)`, `[…]`, `// …`, coloured non-scripture notes | left off, shown in the review |
|
||||
|
||||
**Builds** follow the template:
|
||||
- slides **Build 1, Build 2, …** (no build feature on them): one slide per step. Step 1 is the
|
||||
heading with the first item, each next slide adds one line. Step *k* is a copy of the
|
||||
template's *k*-th build slide (the last one again for longer builds). The heading is a first
|
||||
line ending in `:`/`?`, italic, or above a numbered / same-word list.
|
||||
- one **Build** slide using ProPresenter's build feature: a **\*STREAM DECK\*** slide, then the
|
||||
build with one build step per line.
|
||||
|
||||
With two templates of the same kind, the one-slide-per-step one is used, else the newest.
|
||||
|
||||
Formatting always comes from the template's own sample slides. To change how something looks,
|
||||
change the template in ProPresenter and the tool follows. Templates are found by name: any
|
||||
`_TEMPLATE … DO NOT DELETE …` presentation in Messages, with `GENERIC` marking the generic one.
|
||||
@@ -95,7 +106,8 @@ On themed screens ProPresenter uses the theme's font size, and only words styled
|
||||
from the rest of their box keep bold/italic.
|
||||
- Scriptures too long anywhere are split at verse numbers into as few, even slides as possible,
|
||||
all with the same reference.
|
||||
- Other text too big for the slide is reduced in 1pt steps.
|
||||
- Other text too big for the slide is reported with the size it would fit at; font sizes are
|
||||
never changed (several outputs share them).
|
||||
- Text too long for a theme box is flagged, since the theme fixes the size there.
|
||||
|
||||
## Files
|
||||
|
||||
+20
-5
@@ -33,7 +33,7 @@ import spelling
|
||||
import themes
|
||||
import youtube
|
||||
|
||||
VERSION = "1.3.1"
|
||||
VERSION = "1.3.2"
|
||||
API = None # ProPresenter's network API on this Mac - set in run() from its preferences
|
||||
SUPPORT = os.path.expanduser("~/Library/Application Support/MessageNotes")
|
||||
LOG = os.path.expanduser("~/Library/Logs/message-notes.log")
|
||||
@@ -113,10 +113,20 @@ def find_library(explicit=None):
|
||||
return settings.find_library(explicit=explicit)
|
||||
|
||||
|
||||
def template_rank(path):
|
||||
"""Which of two templates of the same kind to use: one that shows builds as one slide per
|
||||
step (Build 1, Build 2, ...) first, then the most recently saved."""
|
||||
try:
|
||||
step = deck.Template(open(path, "rb").read(), path).step_builds
|
||||
except Exception:
|
||||
step = False
|
||||
return (step, os.path.getmtime(path))
|
||||
|
||||
|
||||
def find_templates(library):
|
||||
out = [os.path.join(library, f) for f in sorted(os.listdir(library)) if settings.is_template(f)]
|
||||
series = sorted([p for p in out if "generic" not in os.path.basename(p).lower()], key=os.path.getmtime, reverse=True)
|
||||
generic = sorted([p for p in out if "generic" in os.path.basename(p).lower()], key=os.path.getmtime, reverse=True)
|
||||
series = sorted([p for p in out if "generic" not in os.path.basename(p).lower()], key=template_rank, reverse=True)
|
||||
generic = sorted([p for p in out if "generic" in os.path.basename(p).lower()], key=template_rank, reverse=True)
|
||||
return series, generic
|
||||
|
||||
|
||||
@@ -155,8 +165,13 @@ def read_notes(path):
|
||||
"""Notes file -> RTF bytes (converting Word with macOS textutil)."""
|
||||
low = path.lower()
|
||||
if low.endswith(".rtf"):
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
except OSError as e:
|
||||
why = "Google Drive isn't running or hasn't downloaded it - open Google Drive, or save the " \
|
||||
"notes into Downloads" if "/CloudStorage/" in path else e.strerror or str(e)
|
||||
raise SystemExit("Could not read %s\n (%s)" % (os.path.basename(path), why))
|
||||
src, tmpdir = path, None
|
||||
if low.endswith(".docx"):
|
||||
tmpdir = tempfile.mkdtemp(prefix="msgnotes-")
|
||||
|
||||
+50
-10
@@ -155,6 +155,11 @@ class Template:
|
||||
self.sample = {}
|
||||
for c, k in zip(self.order, self.kinds):
|
||||
self.sample.setdefault(k, c)
|
||||
# a build is either one slide using ProPresenter's build feature ("Build *DO NOT CLEAR
|
||||
# MID BUILD*"), or one slide per step ("Build 1", "Build 2", ...) - the template decides
|
||||
self.builds = [c for c, k in zip(self.order, self.kinds) if k == "build"]
|
||||
self.step_builds = bool(self.builds) and not any(
|
||||
C.get(el, "build_in") is not None for el in slide_elements(slide_action(self.builds[0])))
|
||||
if "title" not in self.sample:
|
||||
raise TemplateError("The template has no Title slide (a text box named 'Title').")
|
||||
if "scripture" not in self.sample or "point" not in self.sample:
|
||||
@@ -747,6 +752,22 @@ def rebuild_builds(a, el, nlines):
|
||||
set_repeated(bs, "element_build_order", order)
|
||||
|
||||
|
||||
def build_steps(s):
|
||||
"""Lines shown on each slide when a build is one slide per step: the heading stays up
|
||||
with the first item, then one more line per slide."""
|
||||
n = len(s.lines)
|
||||
first = 2 if (n > 1 and s.lines[0].lead) else 1
|
||||
return list(range(min(first, n), n + 1))
|
||||
|
||||
|
||||
def clip_runs(runs, data):
|
||||
"""The measured font runs of a full build, cut to the text one of its steps shows."""
|
||||
if not runs:
|
||||
return runs
|
||||
n16 = len(rtf_plain(data).encode("utf-16-le")) // 2
|
||||
return [dict(r, len=min(r["len"], n16 - r["loc"])) for r in runs if r["loc"] < n16] or None
|
||||
|
||||
|
||||
def build(tpl, plan, name, speaker, layout=None, speaker_runs=None):
|
||||
"""-> .pro bytes. layout: (results, where) from fit(), for exact font runs."""
|
||||
runs = {}
|
||||
@@ -768,7 +789,8 @@ def build(tpl, plan, name, speaker, layout=None, speaker_runs=None):
|
||||
for el in slide_elements(a):
|
||||
nm = element_name(el).lower()
|
||||
if nm in rtfs:
|
||||
set_text(el, rtfs[nm], runs.get((si, nm)), s.scale.get(nm, 1.0), tpl.style(kind, nm))
|
||||
set_text(el, rtfs[nm], clip_runs(runs.get((si, nm)), rtfs[nm]), s.scale.get(nm, 1.0),
|
||||
tpl.style(kind, nm))
|
||||
if nm == "build":
|
||||
rebuild_builds(a, el, len(texts.get("build", [])))
|
||||
set_notes(a, tpl.notes_styles.get(kind) or tpl.default_notes, slide_notes(tpl, s))
|
||||
@@ -795,6 +817,15 @@ def build(tpl, plan, name, speaker, layout=None, speaker_runs=None):
|
||||
continue # e.g. a picture slide with a template that has none
|
||||
if kind == "build" and "prebuild" in tpl.sample:
|
||||
out.append(clone_cue(tpl.sample["prebuild"]))
|
||||
if kind == "build" and tpl.step_builds:
|
||||
# step k from the template's k-th build slide (the last one again for longer builds)
|
||||
for k, shown in enumerate(build_steps(s)):
|
||||
step = notesmod.Slide("build", s.lines[:shown], src=s.src)
|
||||
step.scale = s.scale
|
||||
cue = clone_cue(tpl.builds[min(k, len(tpl.builds) - 1)])
|
||||
fill(cue, step, si)
|
||||
out.append(cue)
|
||||
continue
|
||||
cue = clone_cue(tpl.sample[kind])
|
||||
if kind in ("picture", "clearpicture"):
|
||||
if s.text.strip():
|
||||
@@ -818,8 +849,19 @@ def build(tpl, plan, name, speaker, layout=None, speaker_runs=None):
|
||||
return C.encode(pres)
|
||||
|
||||
|
||||
def cues_for(tpl, s):
|
||||
"""How many slides one plan slide becomes in the built deck."""
|
||||
kind = kind_for(tpl, s)
|
||||
if s.kind == "title" or kind not in tpl.sample:
|
||||
return 0
|
||||
if kind != "build":
|
||||
return 1
|
||||
return (len(build_steps(s)) if tpl.step_builds else 1) + (1 if "prebuild" in tpl.sample else 0)
|
||||
|
||||
|
||||
def slide_numbers(tpl, plan):
|
||||
"""The ProPresenter slide number (1-based) each plan slide will have in the built deck."""
|
||||
"""The ProPresenter slide number (1-based) each plan slide will have in the built deck
|
||||
(a build: its first step)."""
|
||||
nums = []
|
||||
n = len(tpl.head)
|
||||
title_at = tpl.head.index(tpl.kinds.index("title")) + 1
|
||||
@@ -827,20 +869,18 @@ def slide_numbers(tpl, plan):
|
||||
if s.kind == "title":
|
||||
nums.append(title_at)
|
||||
continue
|
||||
kind = kind_for(tpl, s)
|
||||
if kind not in tpl.sample:
|
||||
c = cues_for(tpl, s)
|
||||
if not c:
|
||||
nums.append(None)
|
||||
continue
|
||||
if kind == "build" and "prebuild" in tpl.sample:
|
||||
n += 1
|
||||
n += 1
|
||||
nums.append(n)
|
||||
pre = 1 if (kind_for(tpl, s) == "build" and "prebuild" in tpl.sample) else 0
|
||||
nums.append(n + pre + 1)
|
||||
n += c
|
||||
return nums
|
||||
|
||||
|
||||
def slide_count(tpl, plan):
|
||||
nums = [x for x in slide_numbers(tpl, plan) if x]
|
||||
return max(nums + [len(tpl.head)]) + len(tpl.tail)
|
||||
return len(tpl.head) + sum(cues_for(tpl, s) for s in plan.slides) + len(tpl.tail)
|
||||
|
||||
|
||||
def ensure_pictures(tpl, plan):
|
||||
|
||||
+20
-1
@@ -561,9 +561,11 @@ def statements_to_slides(group, force_build=False):
|
||||
kind = "build" if parallel else "point"
|
||||
else:
|
||||
kind = "build"
|
||||
heading = kind == "build" and build_heading(lines, lead)
|
||||
out_lines = []
|
||||
for k, (text, p, mark, part, start) in enumerate(lines):
|
||||
out_lines.append(Line(underlined(text, p, part, start), lead=(k == 0 and lead and kind == "point")))
|
||||
out_lines.append(Line(underlined(text, p, part, start),
|
||||
lead=(k == 0 and ((lead and kind == "point") or heading))))
|
||||
s = Slide(kind, out_lines, src=src)
|
||||
plain = [p for p in group if not p.bold and not p.italic]
|
||||
if plain and all(p.size <= 15 for p in plain):
|
||||
@@ -573,6 +575,23 @@ def statements_to_slides(group, force_build=False):
|
||||
return [s]
|
||||
|
||||
|
||||
def build_heading(lines, lead):
|
||||
"""Is a build's first line its heading ("What is your FLAW?" over "1. ... 2. ... 3.")?
|
||||
A heading stays up with the first item when a template shows builds as one slide per step."""
|
||||
if len(lines) < 2:
|
||||
return False
|
||||
if lead:
|
||||
return True
|
||||
first, rest = lines[0], lines[1:]
|
||||
if len(rest) < 2:
|
||||
return False
|
||||
numbered = lambda l: bool(l[2] or NUMBERED_RE.match(l[0]))
|
||||
if all(numbered(l) for l in rest) and not numbered(first):
|
||||
return True
|
||||
word = lambda l: l[0].split(" ")[0].lower()
|
||||
return len({word(l) for l in rest}) == 1 and word(first) != word(rest[0])
|
||||
|
||||
|
||||
def is_lead(text, p):
|
||||
t = text.strip()
|
||||
return t.endswith(":") or t.endswith("?") or (p.italic and not p.bold) or (p.italic and p.bold)
|
||||
|
||||
+4
-3
@@ -10,7 +10,8 @@ HEADER = """# Slide list for: {name}
|
||||
# [Title] [Point] [Build] [Subpoint] [Quote] [Sentence] [Scripture] <reference>
|
||||
# [Picture] <what picture> [Clear Picture]
|
||||
# The lines under it are the slide's lines (one line = one paragraph; in a Build, one step).
|
||||
# In a Point, start a line with > to make it the italic lead-in line.
|
||||
# In a Point, start a line with > to make it the italic lead-in line. In a Build, > marks the
|
||||
# heading (it stays up with the first step).
|
||||
# In a Scripture, put **double asterisks** around bold words and *single ones* around italic words.
|
||||
# On any slide, __double underscores__ around words underline them.
|
||||
# Delete a slide by deleting its whole block. Blank lines and lines starting with "# " are ignored.
|
||||
@@ -50,7 +51,7 @@ def dump(plan, name=""):
|
||||
out.append("[%s]" % NAMES.get(s.kind, "Point"))
|
||||
for ln in s.lines:
|
||||
text = "".join(mark(t, False, False, u) for t, _, _, u in ln.segs)
|
||||
out.append(("> " if (ln.lead and s.kind == "point") else "") + text)
|
||||
out.append(("> " if (ln.lead and s.kind in ("point", "build")) else "") + text)
|
||||
for item in left.get(len(plan.slides), []):
|
||||
out.append("#! %s" % item[1].replace("\n", " "))
|
||||
return "\n".join(out) + "\n"
|
||||
@@ -125,7 +126,7 @@ def load(text):
|
||||
t = line.strip()
|
||||
if cur.kind == "scripture":
|
||||
cur.lines.append(notesmod.Line(parse_bold(t)))
|
||||
elif cur.kind == "point" and t.startswith(">"):
|
||||
elif cur.kind in ("point", "build") and t.startswith(">"):
|
||||
cur.lines.append(notesmod.Line(parse_bold(t[1:].strip(), styles=False), lead=True))
|
||||
else:
|
||||
cur.lines.append(notesmod.Line(parse_bold(t, styles=False)))
|
||||
|
||||
BIN
Binary file not shown.
+37
-1
@@ -144,7 +144,43 @@ def main():
|
||||
check("fill-in blanks are not underline marks", plantext.parse_bold("Faith is ______.") ==
|
||||
[("Faith is ______.", False, False, False)])
|
||||
|
||||
# 8. other campuses: their template names, and who preaches there according to the email
|
||||
# 8. a template with one slide per build step (Build 1, Build 2, ...) instead of the build feature
|
||||
STEP = os.path.join(PRO, "_TEMPLATE_GENERIC - DO NOT DELETE 2609-1.pro")
|
||||
stpl = deck.Template(open(STEP, "rb").read())
|
||||
check("step template detected", stpl.step_builds and len(stpl.builds) == 4 and not tpl.step_builds)
|
||||
p8 = notes.parse(open(os.path.join(NOTES, "Slides - Where is Your Faith - Part 8.rtf"), "rb").read())
|
||||
out = slides_of(deck.build(stpl, p8, "TEST", stpl.speaker()))
|
||||
check("step deck slide count", len(out) == deck.slide_count(stpl, p8), "%d vs %d" % (len(out), deck.slide_count(stpl, p8)))
|
||||
steps = [(lab, texts[0][1]) for lab, texts in out if lab.startswith("Build")]
|
||||
flaw = ["What is your FLAW?", "1. FEARFUL INSECURITY - Gideon and Barak", "2. FLESHLY IMPULSES - David and Samson",
|
||||
"3. FAMILY DYSFUNCTION - David and Jephthah"]
|
||||
want = [("Build %d" % k, " / ".join(flaw[:k + 1])) for k in (1, 2, 3)]
|
||||
check("build steps: heading + one more line per slide", steps[:3] == want, "%s" % steps[:3])
|
||||
check("second build: 3 steps from Build 1-3", [s[0] for s in steps[3:]] == ["Build 1", "Build 2", "Build 3"]
|
||||
and steps[5][1].count(" / ") == 3, "%s" % steps[3:])
|
||||
check("no STREAM DECK slide with step builds", not any("STREAM DECK" in lab for lab, _ in out))
|
||||
nums = deck.slide_numbers(stpl, p8)
|
||||
firsts = [nums[i] for i, s in enumerate(p8.slides) if s.kind == "build"]
|
||||
check("slide numbers point at each build's first step", [out[n - 1][0] for n in firsts] == ["Build 1", "Build 1"],
|
||||
"%s" % firsts)
|
||||
five = notes.Plan()
|
||||
five.slides = [p8.slides[0], notes.Slide("build", [notes.Line([("Heading:", False, False)], lead=True)] +
|
||||
[notes.Line([("Item %d" % k, False, False)]) for k in range(1, 6)])]
|
||||
out5 = [(lab, texts[0][1]) for lab, texts in slides_of(deck.build(stpl, five, "TEST", stpl.speaker()))
|
||||
if lab.startswith("Build")]
|
||||
check("a 5-item build: 5 slides, the last template slide reused",
|
||||
[l for l, _ in out5] == ["Build 1", "Build 2", "Build 3", "Build 4", "Build 4"]
|
||||
and out5[-1][1] == "Heading: / Item 1 / Item 2 / Item 3 / Item 4 / Item 5", "%s" % out5)
|
||||
runs = [{"loc": 0, "len": 10}, {"loc": 10, "len": 30}, {"loc": 40, "len": 5}]
|
||||
data = deck.make_rtf(stpl.style("build", "build"), [(0, [("What is your FLAW?", False, False, False)]),
|
||||
(0, [("1. FEARFUL", False, False, False)])])
|
||||
check("font runs cut to a step's text", deck.clip_runs(runs, data) == [{"loc": 0, "len": 10}, {"loc": 10, "len": 19}])
|
||||
rt = plantext.load(plantext.dump(p8, "x"))
|
||||
check("build heading survives the TextEdit round trip",
|
||||
[l.lead for s in p8.slides if s.kind == "build" for l in s.lines] ==
|
||||
[l.lead for s in rt.slides if s.kind == "build" for l in s.lines])
|
||||
|
||||
# 9. other campuses: their template names, and who preaches there according to the email
|
||||
import app
|
||||
import settings
|
||||
for n, want in (("_TEMPLATE - DO NOT DELETE 2608 Lower Thirds.pro", True),
|
||||
|
||||
Reference in New Issue
Block a user