mp 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b1c5ee524e99fecc018dbcc19184f1ae96c0c6a5c6b41d7f4809e754a8b9d1e
4
- data.tar.gz: ba755bb7c1dd73f7cf62ba0b14c07012811dc961750bcace65811df1cd184813
3
+ metadata.gz: 984c7daa2edf2f2b798e1c94fb8d3c045141898029745420d7cae1375344df35
4
+ data.tar.gz: cf7efcb639627fb6ba352a792e6fdf1ee4c067143f7176ff80f42abff3867ca6
5
5
  SHA512:
6
- metadata.gz: ff969b7c6e35a011c35d936cb7b1eafe00c55fcafda636bf7b3ff417b6da0b7483b9421dcbdbe73eb3155099d9a7bc6a3f6d41f632021f28e769f9168bd76964
7
- data.tar.gz: 300a94ef4e03844a3b74151815d72f1e1ddc90902e0acd952954cc4b7e4ae3da5c9ea577b886377a9894f7424b0897a5ea9c31874efc9d80049b1cc6050d41e1
6
+ metadata.gz: 2dee9e1bff8102581fbd35c554f532d1e2130656bb21a135e09d9771a744e8a0287114227007c95aab393cd12e77ffb279e40a613f2f955071746f2a07af8ffb
7
+ data.tar.gz: dad6a20a43f31f7fd908542422af32902b6a5369c72e020a8ca04d2ca63597dd8096c173eb218d1bef0d202e5b483e3bb1444b798671b8ab1d2ba4cc061db67f
data/README.md CHANGED
@@ -106,9 +106,10 @@ style: |
106
106
  <!-- _backgroundColor: black --> このスライドだけに適用(先頭に _)
107
107
  ```
108
108
 
109
- `duration` を指定すると、スライドを表示した時点からタイマーが動き、右下に「ページ番号 / 総枚数」「このページの残り」「全体の残り」を表示します(`paginate` のページ番号はタイマーの中に移ります)。
109
+ `duration` を指定すると、表紙の次のページへ進んだ時点からタイマーが動き(最初のスライドが表紙でなければ表示した時点から)、右下に「ページ番号 / 総枚数」「このページの残り」「全体の残り」を表示します(`paginate` のページ番号はタイマーの中に移ります)。
110
110
 
111
- - 1 ページの持ち時間は、全体の時間を枚数で均等に割った長さです(20 分で 10 枚なら 2 分)
111
+ - 1 ページの持ち時間は、全体の時間を表紙を除いた枚数で均等に割った長さです(20 分で表紙と 10 枚なら 2 分)
112
+ - 表紙にいる間は全体の残りが止まったままで、ページの残りは表示しません
112
113
  - ページごとに使った時間を積算するので、戻って再び開くと続きから数えます
113
114
  - 全体の残りは残り 2 分(短いトークでは全体の 2 割)、ページの残りは持ち時間の 2 割を切ると色が変わり、超過すると赤で超過時間を表示します
114
115
  デッキを保存して再読み込みしてもタイマーは続きます。発表者ビューにも残り時間が出て、`r` で両方をリセットできます。
@@ -157,7 +158,7 @@ bundle exec exe/mp --presenter examples/demo.md
157
158
  | `Home` / `End` | 最初 / 最後 |
158
159
  | 数字 + `Enter` | そのページへ |
159
160
  | `Enter` | 台本のコマンドを実行 |
160
- | `r` | 経過時間をリセット(`duration` のタイマーも戻す) |
161
+ | `r` | 経過時間をリセット(`duration` のタイマーも戻す。表紙にいれば開始前に戻る) |
161
162
  | `+` / `-` | ノートの文字サイズ |
162
163
  | `q` | 終了 |
163
164
 
data/lib/mp/deck.rb CHANGED
@@ -70,7 +70,7 @@ module Mp
70
70
  size: size,
71
71
  duration: duration,
72
72
  style: @globals["style"].to_s,
73
- slides: @slides.map { _1.slice(:html, :notes) },
73
+ slides: @slides.map { _1.slice(:html, :notes, :kind) },
74
74
  embeds: @embeds.transform_values { _1.except(:cwd) }
75
75
  }
76
76
  end
data/lib/mp/server.rb CHANGED
@@ -118,11 +118,17 @@ module Mp
118
118
  def deck_json
119
119
  deck = @deck.to_h
120
120
  embeds = deck[:embeds].transform_values { |embed| embed[:type] == "web" ? embed.merge(src: embed_url(embed[:src])) : embed }
121
- @started_at ||= Time.now # 最初にスライドを表示した時刻からタイマーを始める
122
121
  deck.merge(embeds: embeds, themeCss: theme_css, startedAt: timer_start)
123
122
  end
124
123
 
125
- def timer_start = (@started_at.to_f * 1000).round
124
+ # 先に開始していれば、その時刻を使い続ける
125
+ def start_timer
126
+ started = @lock.synchronize { @started_at.nil? && (@started_at = Time.now) }
127
+ broadcast(JSON.generate(type: "timer", startedAt: timer_start)) if started
128
+ end
129
+
130
+ # 未開始なら nil。開始は聴衆画面が表紙の次へ進んだときに知らせてくる
131
+ def timer_start = @started_at && (@started_at.to_f * 1000).round
126
132
 
127
133
  def embed_url(src)
128
134
  return src if src.empty? || src.match?(%r{\Ahttps?://}i)
@@ -214,9 +220,12 @@ module Mp
214
220
  case JSON.parse(message)["type"]
215
221
  when "quit" then @on_quit&.call
216
222
  when "open-presenter" then @on_open_presenter&.call
223
+ when "timer-start"
224
+ start_timer
217
225
  when "timer-reset"
218
- @started_at = Time.now
219
- broadcast(JSON.generate(type: "timer", startedAt: timer_start))
226
+ # 未開始に戻す。表紙以外にいれば聴衆画面がすぐに開始を送り直す
227
+ @started_at = nil
228
+ broadcast(JSON.generate(type: "timer", startedAt: nil))
220
229
  when "frame" then @on_frame_request&.call
221
230
  else broadcast(message, except: socket)
222
231
  end
data/lib/mp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mp
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
data/public/app.js CHANGED
@@ -39,17 +39,35 @@ async function loadDeck() {
39
39
  show(Math.min(Number(location.hash.slice(1) || 1) - 1, slides.length - 1), { fragments: "none" });
40
40
  }
41
41
 
42
+ // 開始前は数えない
42
43
  function spentOnCurrent(now = Date.now()) {
43
- return (slideSpent.get(current) ?? 0) + (now - enteredAt);
44
+ return startedAt ? (slideSpent.get(current) ?? 0) + (now - enteredAt) : 0;
45
+ }
46
+
47
+ // 待っている表紙には持ち時間がない
48
+ function slideTimed() {
49
+ return !(current === 0 && MpSlides.coverFirst(deck));
50
+ }
51
+
52
+ function slideBudget() {
53
+ return MpSlides.slideBudget(deck, MpSlides.timedSlideCount(deck));
54
+ }
55
+
56
+ // 表紙の次へ進んだら(表紙がなければ最初から)タイマーを始める
57
+ function maybeStartTimer() {
58
+ if (deck?.duration && !startedAt && slideTimed() && events.readyState === WebSocket.OPEN) {
59
+ events.send(JSON.stringify({ type: "timer-start" }));
60
+ }
44
61
  }
45
62
 
46
63
  function renderTimer() {
47
64
  timer.hidden = !deck?.duration || !slides.length;
48
65
  if (timer.hidden) return;
49
66
  const total = MpSlides.remaining(deck, startedAt);
50
- const budget = MpSlides.slideBudget(deck, slides.length);
67
+ const budget = slideBudget();
51
68
  const slide = MpSlides.slideRemaining(budget, spentOnCurrent());
52
69
  timer.querySelector(".timer-pages").textContent = `${current + 1} / ${slides.length}`;
70
+ timer.querySelector(".timer-slide").hidden = !slideTimed();
53
71
  timer.querySelector(".timer-slide b").textContent = MpSlides.formatTime(slide);
54
72
  timer.querySelector(".timer-slide").dataset.state = MpSlides.slideState(budget, slide);
55
73
  timer.querySelector(".timer-total b").textContent = MpSlides.formatTime(total);
@@ -61,6 +79,7 @@ function resetTimer(at) {
61
79
  slideSpent.clear();
62
80
  enteredAt = Date.now();
63
81
  renderTimer();
82
+ maybeStartTimer();
64
83
  sendState();
65
84
  }
66
85
 
@@ -100,7 +119,7 @@ function show(index, { fragments }) {
100
119
  slides[current]?.classList.remove("active");
101
120
  slides[current]?.querySelectorAll("iframe").forEach((frame) => frame.blur());
102
121
  if (index !== current) {
103
- slideSpent.set(current, spentOnCurrent());
122
+ if (startedAt) slideSpent.set(current, spentOnCurrent());
104
123
  enteredAt = Date.now();
105
124
  }
106
125
  current = index;
@@ -111,6 +130,7 @@ function show(index, { fragments }) {
111
130
  mountEmbeds(slide);
112
131
  renderNotes();
113
132
  renderTimer();
133
+ maybeStartTimer();
114
134
  sendState();
115
135
  }
116
136
 
@@ -121,7 +141,9 @@ function sendState() {
121
141
  const terminal = currentTerminal();
122
142
  events.send(JSON.stringify({
123
143
  type: "state", slide: current, step, script: terminal?.script() ?? null, ready: terminal?.ready() ?? false,
124
- slideSpent: spentOnCurrent(), sentAt: Date.now(), // 発表者ビューはここから経過を数える
144
+ // 発表者ビューはここから経過を数える
145
+ slideSpent: spentOnCurrent(), sentAt: Date.now(), running: !!startedAt,
146
+ slideTimed: slideTimed(), slideBudget: deck?.duration ? slideBudget() : null,
125
147
  }));
126
148
  }
127
149
 
@@ -336,7 +358,10 @@ const commands = {
336
358
  };
337
359
 
338
360
  const events = new WebSocket(`${wsBase}events?role=audience`);
339
- events.onopen = sendState;
361
+ events.onopen = () => {
362
+ maybeStartTimer();
363
+ sendState();
364
+ };
340
365
  events.onmessage = (event) => {
341
366
  const message = JSON.parse(event.data);
342
367
  if (message.type === "reload") loadDeck();
data/public/presenter.js CHANGED
@@ -6,7 +6,7 @@ const FRAME_INTERVAL = 250;
6
6
  let deck = null;
7
7
  let state = { slide: 0, step: 0, script: null, ready: false };
8
8
  let digits = "";
9
- let startedAt = Date.now(); // サーバの開始時刻を受け取るまでの仮の値
9
+ let startedAt = null; // 未開始。サーバから開始時刻が届く
10
10
  let noteSize = 26;
11
11
  let mirrorRunning = false;
12
12
  let mirrorSlide = null;
@@ -111,17 +111,19 @@ function onFrame(data) {
111
111
  }
112
112
 
113
113
  function tick() {
114
- $("elapsed").textContent = MpSlides.formatTime(Math.floor((Date.now() - startedAt) / 1000));
114
+ $("elapsed").textContent = MpSlides.formatTime(MpSlides.elapsed(startedAt));
115
115
  $("clock").textContent = new Date().toLocaleTimeString("ja-JP", { hour: "2-digit", minute: "2-digit" });
116
- $("remaining").hidden = $("slide-remaining").hidden = !deck?.duration;
116
+ $("remaining").hidden = !deck?.duration;
117
+ $("slide-remaining").hidden = !deck?.duration || state.slideTimed === false;
117
118
  if (!deck?.duration) return;
118
119
  const remaining = MpSlides.remaining(deck, startedAt);
119
120
  $("remaining").textContent = remaining < 0 ? `超過 ${MpSlides.formatTime(-remaining)}` : `残り ${MpSlides.formatTime(remaining)}`;
120
121
  $("remaining").dataset.state = MpSlides.timerState(deck, remaining);
121
122
 
122
123
  // 聴衆画面が送った使用時間に、送ってからの経過を足す
123
- const budget = MpSlides.slideBudget(deck, deck.slides.length);
124
- const slide = MpSlides.slideRemaining(budget, (state.slideSpent ?? 0) + (Date.now() - (state.sentAt ?? Date.now())));
124
+ const budget = state.slideBudget ?? MpSlides.slideBudget(deck, MpSlides.timedSlideCount(deck));
125
+ const running = state.running ? Date.now() - (state.sentAt ?? Date.now()) : 0;
126
+ const slide = MpSlides.slideRemaining(budget, (state.slideSpent ?? 0) + running);
125
127
  $("slide-remaining").textContent = `このページ ${slide < 0 ? `超過 ${MpSlides.formatTime(-slide)}` : MpSlides.formatTime(slide)}`;
126
128
  $("slide-remaining").dataset.state = MpSlides.slideState(budget, slide);
127
129
  }
data/public/slides.js CHANGED
@@ -22,7 +22,22 @@ const MpSlides = {
22
22
 
23
23
  // 残り時間。超過すると負になる
24
24
  remaining(deck, startedAt, now = Date.now()) {
25
- return deck.duration - Math.floor((now - startedAt) / 1000);
25
+ return deck.duration - MpSlides.elapsed(startedAt, now);
26
+ },
27
+
28
+ // 経過秒。未開始(startedAt が null)なら 0
29
+ elapsed(startedAt, now = Date.now()) {
30
+ return startedAt ? Math.floor((now - startedAt) / 1000) : 0;
31
+ },
32
+
33
+ // 最初のスライドが表紙なら、表紙の次へ進むまでタイマーを始めない
34
+ coverFirst(deck) {
35
+ return deck.slides[0]?.kind === "cover";
36
+ },
37
+
38
+ // 持ち時間を割り当てるページ数(待っている表紙は除く)
39
+ timedSlideCount(deck) {
40
+ return Math.max(deck.slides.length - (MpSlides.coverFirst(deck) ? 1 : 0), 1);
26
41
  },
27
42
 
28
43
  formatTime(seconds) {
data/public/style.css CHANGED
@@ -96,7 +96,7 @@ section.slide > .page-number { bottom: 20px; left: auto; right: 32px; }
96
96
  color: #fff;
97
97
  font: 600 18px/1.3 Menlo, monospace;
98
98
  }
99
- #timer[hidden] { display: none; }
99
+ #timer[hidden], #timer > [hidden] { display: none; }
100
100
  #timer > span { padding: 3px 10px; }
101
101
  #timer > span + span { border-left: 1px solid rgba(255, 255, 255, .25); }
102
102
  #timer small { margin-right: 6px; font: 11px "Hiragino Sans", system-ui, sans-serif; opacity: .8; }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - takkanm