Skip to content

Commit

Permalink
Merge pull request #3 from hanabix/profile
Browse files Browse the repository at this point in the history
个人资料页插入速心比变化趋势
  • Loading branch information
zhongl authored Jul 31, 2024
2 parents a050351 + 2782bf0 commit 6fe296d
Show file tree
Hide file tree
Showing 18 changed files with 104 additions and 33 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

![Activities Chart](./mpb-activities.png)
![Activity Chart](./mpb-activity.png)
![Profile Chart](./mpb-profile.png)

这是一款面向 **入门跑者****[MAF训练法][maf]跑者** 的,聚焦于跑步训练中 **[速心比](#速心比meters-per-beat)** 指标数据可视化的浏览器扩展程序(俗称:插件)。

Expand Down
Binary file added mpb-profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified mpb-workout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import org.scalajs.dom.*
object Main:

def main(args: Array[String]): Unit =
val route = OnPageActivities() orElse OnPageActivity() orElse OnPageWorkout()
val route = OnPageActivities() orElse OnPageActivity() orElse OnPageWorkout() orElse OnPageProfile()
MutationObserver: (rs, _) =>
route.applyOrElse((new URL(window.location.href), rs.toSeq), _ => ())
.observe(
Expand Down
8 changes: 5 additions & 3 deletions src/OnPageActivities.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import plot.*

object OnPageActivities:

def apply()(using a: Anchor[HTMLElement], s: Scatter[Activities]): Route =
def apply()(using a: Anchor[HTMLElement], s: Scatter[SearchResult]): Route =
case (Activities(tp), HasListItem()) =>
val root = a.init(document.querySelector("div.advanced-filtering").before(_), "mpb")
if tp != "running" then root.remove()
else for gas <- Service.activities(SearchFilter(tp.get)) do Plot(root, Array(gas), "速心比变化趋势")(using _.scatter)
if tp == "running" || tp == undefined then
for gas <- Service.activities(SearchFilter("running")) if gas.nonEmpty do
Plot(root, Array(gas), "速心比变化趋势")(using _.scatter)
else root.remove()
end apply

private val Activities = Extract[URL, UndefOr[String]]:
Expand Down
2 changes: 1 addition & 1 deletion src/OnPageActivity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object OnPageActivity:
t <- a.activityTypeDTO
k <- t.typeKey if k == "running"
do
for laps <- Service.laps(id.toDouble) do
for laps <- Service.laps(id.toDouble) if laps.nonEmpty do
Plot(aa.init(e.before(_), "scatter"), Arr(laps), "速心比变化趋势")(using _.scatter)
end apply

Expand Down
44 changes: 44 additions & 0 deletions src/OnPageProfile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import scala.concurrent.ExecutionContext.Implicits.global
import scala.scalajs.js

import org.scalajs.dom.*

import common.Anchor
import common.Extract
import common.Functional.*
import common.GetElement.query
import common.GetMutationRecord.addedNodes
import common.GetURL.path
import common.Regex.*
import garmin.Pagination
import garmin.SearchResult
import garmin.Service
import plot.Plot
import plot.Scatter

object OnPageProfile:
def apply()(using a: Anchor[HTMLElement], s: Scatter[SearchResult]): Route =
case (Profile(Seq(_, id: String)), PageContent(e)) =>
val p: Pagination = new:
start = 1
limit = 30
for
ap <- Service.activitiesByProfile(id, p)
al <- ap.activityList
do
val sr = al.filter(_.activityType.get.typeKey == "running")
if sr.nonEmpty then
val layout = Plot.Layout("近期跑步速心比变化趋势")
val config = Plot.Config().setDisplayModeBar(false)
Plot(a.init(e.before(_), "scatter"), js.Array(sr), layout, config)(using _.scatter)
end apply

private val Profile = Extract[URL, Seq[js.UndefOr[String]]]:
path |> "/modern/profile/(.+)".capture

private val PageContent = Extract[Seq[MutationRecord], Element]:
inline def E = Extract[Element, Element]:
query("div[class^=\"PageContent\"]").??

_.flatMap(addedNodes[Element]).collectFirst({ case E(e) => e })
end OnPageProfile
12 changes: 7 additions & 5 deletions src/OnPageWorkout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import garmin.*
import plot.*

object OnPageWorkout:
def apply()(using a: Anchor[HTMLElement], s: Scatter[ActivityLaps], b: Box[ActivityLaps]): Route =
def apply()(using a: Anchor[HTMLElement], s: Scatter[ActivityByWorkout], b: Box[ActivityByWorkout]): Route =
case (Workout(Seq(_, id: String), "running"), PageHeader(e)) =>
for list <- Service.activityLapsList(id.toDouble, 7) do
val r = list.reverse
Plot(a.init(e.appendChild, "box"), r, "近七日速心比分布趋势")(using _.box)
Plot(a.init(e.appendChild, "scatter"), r, "近七日速心比趋势对比")(using _.scatter)
for list <- Service.activityLapsList(id.toDouble, 30) if list.nonEmpty do
val limit = Math.min(list.length, 14)
val latest = Math.min(limit, 5)
val layout = Plot.Layout(s"最近${limit}次速心比分布趋势").setShowlegend(false)
Plot(a.init(e.appendChild, "box"), list.take(limit).reverse, layout)(using _.box)
Plot(a.init(e.appendChild, "scatter"), list.take(latest).reverse, s"${latest}次速心比趋势对比")(using _.scatter)

private val Workout = Extract[URL, (Seq[UndefOr[String]], UndefOr[String])]:
(path |> "/modern/workout/(\\d+)".capture) ~> param("workoutType")
Expand Down
4 changes: 2 additions & 2 deletions src/common/GetMutationRecord.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import org.scalajs.dom.*

object GetMutationRecord:
import scala.reflect.Typeable
inline def addedNodes[A >: Element: Typeable]: MutationRecord => Seq[A] =
_.addedNodes.toSeq.collect({ case a: A => a })
inline def addedNodes[A >: Element: Typeable]: MutationRecord => Seq[A] = r =>
for case a: A <- r.addedNodes.toSeq yield a
6 changes: 6 additions & 0 deletions src/garmin/ActivitiesByProfile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package garmin

import scala.scalajs.js

trait ActivitiesByProfile extends js.Object:
var activityList: js.UndefOr[SearchResult] = js.undefined
3 changes: 2 additions & 1 deletion src/garmin/Activity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import scala.scalajs.js
trait Activity extends Performance:
var activityId: js.UndefOr[Double] = js.undefined
var activityName: js.UndefOr[String] = js.undefined
var summaryDTO: js.UndefOr[Performance] = js.undefined
var summaryDTO: js.UndefOr[Performance] = js.undefined
var activityTypeDTO: js.UndefOr[ActivityType] = js.undefined
var activityType: js.UndefOr[ActivityType] = js.undefined
8 changes: 8 additions & 0 deletions src/garmin/Pagination.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package garmin

import scala.scalajs.js

trait Pagination extends js.Object:
var excludeChildren: js.UndefOr[Boolean] = js.undefined
var start: js.UndefOr[Int | String] = js.undefined
var limit: js.UndefOr[Int | String] = js.undefined
5 changes: 1 addition & 4 deletions src/garmin/SearchFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ package garmin
import scala.scalajs.js
import scala.scalajs.js.annotation.JSName

trait SearchFilter extends js.Object:
trait SearchFilter extends Pagination:
var activityType: js.UndefOr[String] = js.undefined
var start: js.UndefOr[Int | String] = js.undefined
var limit: js.UndefOr[Int | String] = js.undefined
var excludeChildren: js.UndefOr[Boolean] = js.undefined
var startDate: js.UndefOr[String] = js.undefined
@JSName("_") var now: js.UndefOr[Double] = js.undefined

Expand Down
13 changes: 8 additions & 5 deletions src/garmin/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import common.*
import common.Functional.*

object Service:
def activityLapsList(workoutId: Double, latestDays: Double)(using
def activityLapsList(workoutId: Double, limit: Int)(using
DateFormat[Double]
): Future[js.Array[ActivityLaps]] =
val cur = js.Date.now()
): Future[js.Array[ActivityByWorkout]] =
val filter = SearchFilter("running")
filter.startDate = cur.dayBefore(7).ymd("fr-CA")
filter.limit = limit

activities(filter)
.flatMap: sa =>
Expand All @@ -25,14 +24,18 @@ object Service:
.map(_.toJSArray)
end activityLapsList

def activitiesByProfile(id: String, p: Pagination) =
val url = s"${base}/activitylist-service/activities/$id${params(p)}"
get[ActivitiesByProfile](url, s"https://connect.garmin.cn/modern/profile/$id")

def activity(id: Double) =
val url = s"${base}/activity-service/activity/$id?_=${js.Date.now()}"
get[Activity](url, s"https://connect.garmin.cn/modern/activity/$id")

def activities(filter: SearchFilter) =
val url = s"${base}/activitylist-service/activities/search/activities${params(filter)}"
// TODO improve referer
get[Activities](url, "https://connect.garmin.cn/modern/activities")
get[SearchResult](url, "https://connect.garmin.cn/modern/activities")

def laps(activityId: Double) =
inline def equal[A](a: A): js.UndefOr[A] => Boolean = a == _
Expand Down
8 changes: 4 additions & 4 deletions src/garmin/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import scala.scalajs.js

inline val base = "https://connect.garmin.cn"

type Activities = js.Array[ActivityItem]
type Laps = js.Array[Lap]
type ActivityItem = Activity & Workout & Performance
type ActivityLaps = (Activity & Workout, Laps)
type SearchResult = js.Array[ActivityBySearch]
type Laps = js.Array[Lap]
type ActivityBySearch = Activity & Workout & Performance
type ActivityByWorkout = (Activity & Workout, Laps)
4 changes: 2 additions & 2 deletions src/plot/Box.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ object Box:
.PartialPlotDataAutobinx()
.setY(laps.map(_.mpb))
.setHoverinfo(cs.y)
.setWidth(0.1)
.setWidth(0.3)
.setType(cs.box)

given activityLaps(using Box[Laps], DateFormat[String]): Box[ActivityLaps] = ga =>
given activityLaps(using Box[Laps], DateFormat[String]): Box[ActivityByWorkout] = ga =>
ga match
case (a, laps) =>
laps.box
Expand Down
13 changes: 10 additions & 3 deletions src/plot/Plot.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package plot

import scala.scalajs.js.*
import scala.scalajs.js

import org.scalajs.dom.*

Expand All @@ -9,8 +9,15 @@ import typings.plotlyJs.mod.*
import typings.plotlyJsDistMin.mod

object Plot:
def apply[A](root: HTMLElement, data: Array[A], title: String)(using Conversion[A, Data]) =
mod.newPlot(root, data.map(_.convert), Layout(title), Config())
def apply[A](root: HTMLElement, data: js.Array[A], title: String)(using
Conversion[A, Data]
): js.Promise[PlotlyHTMLElement] =
apply(root, data, Layout(title), Config())

def apply[A](root: HTMLElement, data: js.Array[A], layout: PartialLayout, config: PartialConfig = Config())(using
Conversion[A, Data]
): js.Promise[PlotlyHTMLElement] =
mod.newPlot(root, data.map(_.convert), layout, config)

object Layout:
def apply(title: String) = PartialLayout()
Expand Down
4 changes: 2 additions & 2 deletions src/plot/Scatter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ object Scatter:
.setHovertext(laps.map(_.text))
.setHovertemplate("<b>%{y:.3f}</b><br>%{hovertext}<extra></extra>")

given activityLaps(using Scatter[Laps], DateFormat[String]): Scatter[ActivityLaps] = ga =>
given activityLaps(using Scatter[Laps], DateFormat[String]): Scatter[ActivityByWorkout] = ga =>
ga match
case (a, laps) =>
laps.scatter
Expand All @@ -36,7 +36,7 @@ object Scatter:

given activities(using
MetersPerBeat[Performance]
): Scatter[js.Array[ActivityItem]] = gas =>
): Scatter[SearchResult] = gas =>
Data
.PartialPlotDataAutobinx()
.setY(gas.map(_.mpb))
Expand Down

0 comments on commit 6fe296d

Please sign in to comment.