mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(stats): record android as a reading source
Accept android as a reading-session source from clients and report it as its own statistics bucket, labelled Android app, beside the iOS app and Apple Watch. A generated migration adds it to the reading_sessions check constraint, achievements count Android reading toward Full Orbit and Two Worlds, and the web charts draw it in a new --pill-android token at hue 95.
This commit is contained in:
@@ -144,6 +144,7 @@
|
||||
--pill-web: oklch(0.55 0.16 245);
|
||||
--pill-ios: oklch(0.58 0.16 150);
|
||||
--pill-watchos: oklch(0.58 0.18 35);
|
||||
--pill-android: oklch(0.58 0.11 95);
|
||||
--pill-koreader: oklch(0.52 0.2 295);
|
||||
--pill-kobo: oklch(0.55 0.1 195);
|
||||
--pill-repaired: oklch(0.62 0.14 70);
|
||||
@@ -381,6 +382,7 @@
|
||||
--pill-web: oklch(0.72 0.14 245);
|
||||
--pill-ios: oklch(0.75 0.14 150);
|
||||
--pill-watchos: oklch(0.76 0.15 35);
|
||||
--pill-android: oklch(0.75 0.14 95);
|
||||
--pill-koreader: oklch(0.72 0.17 295);
|
||||
--pill-kobo: oklch(0.74 0.1 195);
|
||||
--pill-repaired: oklch(0.78 0.13 70);
|
||||
|
||||
@@ -75,7 +75,7 @@ function genreLabel(genre: BroadReadingGenre): string {
|
||||
}
|
||||
|
||||
function sourceLabel(source: string): string {
|
||||
if (['web', 'ios', 'watchos', 'koreader', 'manual', 'kobo', 'unknown'].includes(source)) {
|
||||
if (['web', 'ios', 'watchos', 'android', 'koreader', 'manual', 'kobo', 'unknown'].includes(source)) {
|
||||
return t(`adminFeature.sharedInsights.sourceNames.${source}`)
|
||||
}
|
||||
return source
|
||||
|
||||
@@ -40,6 +40,7 @@ const BUCKET_TOKEN: Record<ReadingSessionSourceBucket, string> = {
|
||||
bookorbit: '--pill-web',
|
||||
ios: '--pill-ios',
|
||||
watchos: '--pill-watchos',
|
||||
android: '--pill-android',
|
||||
koreader: '--pill-koreader',
|
||||
kobo: '--pill-kobo',
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const BUCKET_TOKEN: Record<ReadingSessionSourceBucket, string> = {
|
||||
bookorbit: '--pill-web',
|
||||
ios: '--pill-ios',
|
||||
watchos: '--pill-watchos',
|
||||
android: '--pill-android',
|
||||
koreader: '--pill-koreader',
|
||||
kobo: '--pill-kobo',
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const BUCKET_TOKEN: Record<ReadingSessionSourceBucket, string> = {
|
||||
bookorbit: '--pill-web',
|
||||
ios: '--pill-ios',
|
||||
watchos: '--pill-watchos',
|
||||
android: '--pill-android',
|
||||
koreader: '--pill-koreader',
|
||||
kobo: '--pill-kobo',
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ watchEffect(() => {
|
||||
:error
|
||||
:empty="isEmpty"
|
||||
:empty-title="t('statistics.charts.sourceDistribution.emptyTitle')"
|
||||
:empty-description="t('statistics.charts.sourceDistribution.emptyDescriptionWithAppleClients')"
|
||||
:empty-description="t('statistics.charts.sourceDistribution.emptyDescriptionWithNativeClients')"
|
||||
>
|
||||
<VChart :option autoresize style="height: 100%" />
|
||||
</ChartCard>
|
||||
|
||||
@@ -15,8 +15,8 @@ describe('breakdown', () => {
|
||||
|
||||
it('returns all source buckets for the source dimension', () => {
|
||||
const series = getBreakdownSeries('source', 'theme:violet', [])
|
||||
expect(series.map((s) => s.key)).toEqual(['bookorbit', 'ios', 'watchos', 'koreader', 'kobo'])
|
||||
expect(series.map((s) => s.label)).toEqual(['BookOrbit', 'iOS app', 'Apple Watch', 'KOReader', 'Kobo'])
|
||||
expect(series.map((s) => s.key)).toEqual(['bookorbit', 'ios', 'watchos', 'android', 'koreader', 'kobo'])
|
||||
expect(series.map((s) => s.label)).toEqual(['BookOrbit', 'iOS app', 'Apple Watch', 'Android app', 'KOReader', 'Kobo'])
|
||||
})
|
||||
|
||||
it('returns one series per format key for the format dimension', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => ({
|
||||
'--pill-web': 'oklch(0.72 0.14 245)',
|
||||
'--pill-ios': 'oklch(0.75 0.14 150)',
|
||||
'--pill-watchos': 'oklch(0.76 0.15 35)',
|
||||
'--pill-android': 'oklch(0.75 0.14 95)',
|
||||
'--pill-koreader': 'oklch(0.72 0.17 295)',
|
||||
'--pill-kobo': 'oklch(0.74 0.1 195)',
|
||||
}) as Record<string, string>
|
||||
@@ -30,6 +31,7 @@ describe('source-bucket-colors', () => {
|
||||
bookorbit: '--pill-web',
|
||||
ios: '--pill-ios',
|
||||
watchos: '--pill-watchos',
|
||||
android: '--pill-android',
|
||||
koreader: '--pill-koreader',
|
||||
kobo: '--pill-kobo',
|
||||
})
|
||||
@@ -38,13 +40,14 @@ describe('source-bucket-colors', () => {
|
||||
it('converts oklch tokens to a zrender-parseable hex so bars do not vanish on hover', () => {
|
||||
const colors = resolveSourceBucketColors('dark:violet')
|
||||
|
||||
expect(colors).toEqual({ bookorbit: '#abcdef', ios: '#abcdef', watchos: '#abcdef', koreader: '#abcdef', kobo: '#abcdef' })
|
||||
expect(colors).toEqual({ bookorbit: '#abcdef', ios: '#abcdef', watchos: '#abcdef', android: '#abcdef', koreader: '#abcdef', kobo: '#abcdef' })
|
||||
expect(mocks.readCssColor).toHaveBeenCalledWith('--pill-web')
|
||||
expect(mocks.readCssColor).toHaveBeenCalledWith('--pill-ios')
|
||||
expect(mocks.readCssColor).toHaveBeenCalledWith('--pill-watchos')
|
||||
expect(mocks.readCssColor).toHaveBeenCalledWith('--pill-android')
|
||||
expect(mocks.readCssColor).toHaveBeenCalledWith('--pill-koreader')
|
||||
expect(mocks.readCssColor).toHaveBeenCalledWith('--pill-kobo')
|
||||
expect(mocks.oklchToHex).toHaveBeenCalledTimes(5)
|
||||
expect(mocks.oklchToHex).toHaveBeenCalledTimes(6)
|
||||
})
|
||||
|
||||
it('passes through values that are already rgb/hex', () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ export const SOURCE_BUCKET_COLOR_TOKENS: Record<ReadingSessionSourceBucket, stri
|
||||
bookorbit: '--pill-web',
|
||||
ios: '--pill-ios',
|
||||
watchos: '--pill-watchos',
|
||||
android: '--pill-android',
|
||||
koreader: '--pill-koreader',
|
||||
kobo: '--pill-kobo',
|
||||
}
|
||||
@@ -43,6 +44,7 @@ export function resolveSourceBucketColors(themeKey: string): Record<ReadingSessi
|
||||
bookorbit: toParseableColor(readCssColor(SOURCE_BUCKET_COLOR_TOKENS.bookorbit)),
|
||||
ios: toParseableColor(readCssColor(SOURCE_BUCKET_COLOR_TOKENS.ios)),
|
||||
watchos: toParseableColor(readCssColor(SOURCE_BUCKET_COLOR_TOKENS.watchos)),
|
||||
android: toParseableColor(readCssColor(SOURCE_BUCKET_COLOR_TOKENS.android)),
|
||||
koreader: toParseableColor(readCssColor(SOURCE_BUCKET_COLOR_TOKENS.koreader)),
|
||||
kobo: toParseableColor(readCssColor(SOURCE_BUCKET_COLOR_TOKENS.kobo)),
|
||||
}
|
||||
|
||||
@@ -3642,6 +3642,7 @@
|
||||
"web": "Web reader",
|
||||
"ios": "iOS app",
|
||||
"watchos": "Apple Watch",
|
||||
"android": "Android app",
|
||||
"koreader": "KOReader",
|
||||
"manual": "Manual",
|
||||
"kobo": "Kobo",
|
||||
@@ -9826,7 +9827,8 @@
|
||||
"title": "Where You Read",
|
||||
"emptyTitle": "No reading activity yet",
|
||||
"emptyDescription": "Read on the web, KOReader, or Kobo to see your source split.",
|
||||
"emptyDescriptionWithAppleClients": "Read on the web, iOS app, Apple Watch, KOReader, or Kobo to see your source split."
|
||||
"emptyDescriptionWithAppleClients": "Read on the web, iOS app, Apple Watch, KOReader, or Kobo to see your source split.",
|
||||
"emptyDescriptionWithNativeClients": "Read on the web, iOS app, Android app, Apple Watch, KOReader, or Kobo to see your source split."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user