first commit
This commit is contained in:
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# 依赖目录
|
||||
node_modules/
|
||||
|
||||
# 构建产物
|
||||
**/dist/
|
||||
**/coverage/
|
||||
|
||||
# 本地环境变量
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# 日志与缓存
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
.npm-cache/
|
||||
data/tts-cache/
|
||||
|
||||
# 小程序本地运维配置(由 .env 生成)
|
||||
apps/miniprogram/utils/opsEnv.js
|
||||
|
||||
calendar/node_modules
|
||||
dash/backups
|
||||
dash/downloads
|
||||
|
||||
/dist
|
||||
/kindle-dash-*.tgz
|
||||
/tmp
|
||||
12
calendar/index.html
Normal file
12
calendar/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Calendar Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1226
calendar/package-lock.json
generated
Normal file
1226
calendar/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
calendar/package.json
Normal file
22
calendar/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "calendar",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lunar-typescript": "^1.8.6",
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0",
|
||||
"vue-tsc": "^3.2.5"
|
||||
}
|
||||
}
|
||||
72
calendar/src/App.vue
Normal file
72
calendar/src/App.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import CalendarCard from '@/components/CalendarCard.vue';
|
||||
import QuoteCard from '@/components/QuoteCard.vue';
|
||||
import WeatherCard from '@/components/WeatherCard.vue';
|
||||
import { buildCalendarModel } from '@/lib/calendar';
|
||||
import { getQuoteForDate } from '@/lib/quotes';
|
||||
import { fetchWeather, resolveLocation, type LocationCoordinates, type WeatherSnapshot } from '@/lib/weather';
|
||||
|
||||
const now = ref(new Date());
|
||||
const location = ref<LocationCoordinates>({
|
||||
latitude: 31.2304,
|
||||
longitude: 121.4737,
|
||||
label: '上海',
|
||||
});
|
||||
const weather = ref<WeatherSnapshot | null>(null);
|
||||
const weatherStatus = ref<'idle' | 'loading' | 'ready' | 'error'>('idle');
|
||||
|
||||
let clockTimer = 0;
|
||||
let weatherTimer = 0;
|
||||
|
||||
const calendarModel = computed(() => buildCalendarModel(now.value));
|
||||
const quoteEntry = computed(() => getQuoteForDate(now.value));
|
||||
|
||||
async function refreshWeather() {
|
||||
weatherStatus.value = 'loading';
|
||||
|
||||
try {
|
||||
weather.value = await fetchWeather(location.value);
|
||||
weatherStatus.value = 'ready';
|
||||
} catch (error) {
|
||||
console.error('获取天气失败', error);
|
||||
weatherStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
location.value = await resolveLocation();
|
||||
await refreshWeather();
|
||||
|
||||
clockTimer = window.setInterval(() => {
|
||||
now.value = new Date();
|
||||
}, 30 * 1000);
|
||||
|
||||
// 天气不需要秒级刷新,30 分钟一次足够。
|
||||
weatherTimer = window.setInterval(() => {
|
||||
void refreshWeather();
|
||||
}, 30 * 60 * 1000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(clockTimer);
|
||||
window.clearInterval(weatherTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="page-shell">
|
||||
<div class="dashboard-frame">
|
||||
<div class="dashboard-grid">
|
||||
<CalendarCard :model="calendarModel" />
|
||||
<WeatherCard
|
||||
:weather="weather"
|
||||
:status="weatherStatus"
|
||||
:location-label="location.label"
|
||||
/>
|
||||
<QuoteCard :quote="quoteEntry.text" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
70
calendar/src/components/CalendarCard.vue
Normal file
70
calendar/src/components/CalendarCard.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { CalendarModel } from '@/lib/calendar';
|
||||
|
||||
const props = defineProps<{
|
||||
model: CalendarModel;
|
||||
}>();
|
||||
|
||||
const weeks = computed(() => {
|
||||
const chunks: CalendarModel['cells'][] = [];
|
||||
|
||||
for (let index = 0; index < props.model.cells.length; index += 7) {
|
||||
chunks.push(props.model.cells.slice(index, index + 7));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="card calendar-card">
|
||||
<div class="calendar-card__header">
|
||||
<div class="calendar-card__day">{{ model.largeDay }}</div>
|
||||
<div class="calendar-card__meta">
|
||||
<p class="calendar-card__date-line">
|
||||
<span>{{ model.gregorianLabel }}</span>
|
||||
<span class="calendar-card__weekday">{{ model.weekdayLabel }}</span>
|
||||
</p>
|
||||
<p class="calendar-card__lunar">
|
||||
<span>{{ model.lunarYearLabel }}</span>
|
||||
<span>{{ model.lunarDayLabel }}</span>
|
||||
<span v-if="model.summaryBadges.length" class="calendar-card__badges calendar-card__badges--inline">
|
||||
<span
|
||||
v-for="badge in model.summaryBadges"
|
||||
:key="badge.label"
|
||||
:class="['calendar-card__badge', `calendar-card__badge--${badge.tone}`]"
|
||||
>
|
||||
{{ badge.label }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-card__grid">
|
||||
<div v-for="label in model.weekLabels" :key="label" class="calendar-card__week-label">
|
||||
{{ label }}
|
||||
</div>
|
||||
|
||||
<template v-for="week in weeks" :key="week[0].date.toISOString()">
|
||||
<div
|
||||
v-for="cell in week"
|
||||
:key="cell.date.toISOString()"
|
||||
:class="[
|
||||
'calendar-card__cell',
|
||||
{
|
||||
'calendar-card__cell--muted': !cell.currentMonth,
|
||||
'calendar-card__cell--today': cell.isToday,
|
||||
'calendar-card__cell--holiday': cell.badges.some((badge) => badge.tone === 'holiday'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<span class="calendar-card__solar">{{ cell.day }}</span>
|
||||
<span class="calendar-card__lunar-day">{{ cell.subLabel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
27
calendar/src/components/QuoteCard.vue
Normal file
27
calendar/src/components/QuoteCard.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
quote: string;
|
||||
}>();
|
||||
|
||||
const quoteFontSize = computed(() => {
|
||||
const length = props.quote.length;
|
||||
|
||||
if (length > 140) {
|
||||
return '1.75rem';
|
||||
}
|
||||
|
||||
if (length > 100) {
|
||||
return '1.95rem';
|
||||
}
|
||||
|
||||
return '2.2rem';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="card quote-card">
|
||||
<p class="quote-card__content" :style="{ fontSize: quoteFontSize }">{{ quote }}</p>
|
||||
</section>
|
||||
</template>
|
||||
158
calendar/src/components/WeatherCard.vue
Normal file
158
calendar/src/components/WeatherCard.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import WeatherGlyph from './WeatherGlyph.vue';
|
||||
import type { ForecastDay, WeatherSnapshot } from '@/lib/weather';
|
||||
|
||||
const props = defineProps<{
|
||||
weather: WeatherSnapshot | null;
|
||||
status: 'idle' | 'loading' | 'ready' | 'error';
|
||||
locationLabel: string;
|
||||
}>();
|
||||
|
||||
function weatherKind(code: number) {
|
||||
if ([0].includes(code)) {
|
||||
return 'clear';
|
||||
}
|
||||
|
||||
if ([1, 2].includes(code)) {
|
||||
return 'partly';
|
||||
}
|
||||
|
||||
if ([3].includes(code)) {
|
||||
return 'cloudy';
|
||||
}
|
||||
|
||||
if ([45, 48].includes(code)) {
|
||||
return 'fog';
|
||||
}
|
||||
|
||||
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code)) {
|
||||
return 'rain';
|
||||
}
|
||||
|
||||
if ([71, 73, 75, 77, 85, 86].includes(code)) {
|
||||
return 'snow';
|
||||
}
|
||||
|
||||
if ([95, 96, 99].includes(code)) {
|
||||
return 'storm';
|
||||
}
|
||||
|
||||
return 'cloudy';
|
||||
}
|
||||
|
||||
const forecast = computed(() => props.weather?.forecast.slice(0, 5) ?? []);
|
||||
|
||||
const metrics = computed(() => {
|
||||
if (!props.weather) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: '日出',
|
||||
value: props.weather.sunrise,
|
||||
accent: 'metric--sunrise',
|
||||
icon: 'sunrise',
|
||||
},
|
||||
{
|
||||
label: '日落',
|
||||
value: props.weather.sunset,
|
||||
accent: 'metric--sunset',
|
||||
icon: 'sunset',
|
||||
},
|
||||
{
|
||||
label: '空气质量',
|
||||
value:
|
||||
props.weather.aqi === null
|
||||
? '暂无'
|
||||
: `${props.weather.aqi}${props.weather.aqiLabel}`,
|
||||
accent: 'metric--air',
|
||||
icon: 'air',
|
||||
},
|
||||
{
|
||||
label: '能见度',
|
||||
value: `${props.weather.visibilityKm} km`,
|
||||
accent: 'metric--visibility',
|
||||
icon: 'visibility',
|
||||
},
|
||||
] as const;
|
||||
});
|
||||
|
||||
const stateLabel = computed(() => {
|
||||
if (props.status === 'error') {
|
||||
return '天气数据获取失败';
|
||||
}
|
||||
|
||||
if (props.status === 'loading') {
|
||||
return '天气数据加载中';
|
||||
}
|
||||
|
||||
return props.locationLabel;
|
||||
});
|
||||
|
||||
const currentWeatherKind = computed(() => weatherKind(props.weather?.weatherCode ?? 3));
|
||||
|
||||
function forecastKind(day: ForecastDay) {
|
||||
return weatherKind(day.weatherCode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="card weather-card">
|
||||
<div class="weather-card__heading">
|
||||
<div>
|
||||
<p class="weather-card__title">天气预报</p>
|
||||
<p class="weather-card__subtitle">{{ stateLabel }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="weather" class="weather-card__hero">
|
||||
<div class="weather-card__hero-main">
|
||||
<WeatherGlyph :kind="currentWeatherKind" large />
|
||||
<div>
|
||||
<div class="weather-card__temperature">{{ weather.temperature }}°C</div>
|
||||
<div class="weather-card__condition">{{ weather.condition }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="weather-card__facts">
|
||||
<div class="weather-card__fact">
|
||||
<WeatherGlyph kind="humidity" />
|
||||
<span>湿度 {{ weather.humidity }}%</span>
|
||||
</div>
|
||||
<div class="weather-card__fact">
|
||||
<WeatherGlyph kind="wind" />
|
||||
<span>风速 {{ weather.windSpeed }} km/h</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="weather-card__hero weather-card__hero--placeholder">
|
||||
<p>{{ stateLabel }}</p>
|
||||
</div>
|
||||
|
||||
<div class="weather-card__forecast">
|
||||
<article v-for="day in forecast" :key="day.label" class="forecast-pill">
|
||||
<p class="forecast-pill__label">{{ day.label }}</p>
|
||||
<WeatherGlyph :kind="forecastKind(day)" large />
|
||||
<p class="forecast-pill__temp">{{ day.high }}°<span>{{ day.low }}°</span></p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="weather-card__metrics">
|
||||
<article
|
||||
v-for="metric in metrics"
|
||||
:key="metric.label"
|
||||
:class="['metric-pill', metric.accent]"
|
||||
>
|
||||
<div class="metric-pill__label">
|
||||
<WeatherGlyph :kind="metric.icon" />
|
||||
<span>{{ metric.label }}</span>
|
||||
</div>
|
||||
<p class="metric-pill__value">{{ metric.value }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
154
calendar/src/components/WeatherGlyph.vue
Normal file
154
calendar/src/components/WeatherGlyph.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
type WeatherGlyphKind =
|
||||
| 'clear'
|
||||
| 'partly'
|
||||
| 'cloudy'
|
||||
| 'fog'
|
||||
| 'rain'
|
||||
| 'snow'
|
||||
| 'storm'
|
||||
| 'sunrise'
|
||||
| 'sunset'
|
||||
| 'air'
|
||||
| 'visibility'
|
||||
| 'humidity'
|
||||
| 'wind';
|
||||
|
||||
const props = defineProps<{
|
||||
kind: WeatherGlyphKind;
|
||||
large?: boolean;
|
||||
}>();
|
||||
|
||||
// 统一收细线宽和轮廓,让图标在墨水屏和小尺寸卡片里更精致、更稳定。
|
||||
const strokeWidth = computed(() => (props.large ? 1.95 : 1.8));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:class="['glyph', { 'glyph--large': props.large }]"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:stroke-width="strokeWidth"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g v-if="props.kind === 'clear'">
|
||||
<circle cx="24" cy="24" r="6.5" />
|
||||
<line x1="24" y1="8.5" x2="24" y2="12.5" />
|
||||
<line x1="24" y1="35.5" x2="24" y2="39.5" />
|
||||
<line x1="8.5" y1="24" x2="12.5" y2="24" />
|
||||
<line x1="35.5" y1="24" x2="39.5" y2="24" />
|
||||
<line x1="13.1" y1="13.1" x2="15.9" y2="15.9" />
|
||||
<line x1="32.1" y1="32.1" x2="34.9" y2="34.9" />
|
||||
<line x1="13.1" y1="34.9" x2="15.9" y2="32.1" />
|
||||
<line x1="32.1" y1="15.9" x2="34.9" y2="13.1" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'partly'">
|
||||
<circle cx="16.5" cy="16.5" r="5" />
|
||||
<line x1="16.5" y1="7.5" x2="16.5" y2="10.5" />
|
||||
<line x1="9.2" y1="16.5" x2="12.2" y2="16.5" />
|
||||
<line x1="11.4" y1="11.4" x2="13.6" y2="13.6" />
|
||||
<line x1="21.6" y1="11.4" x2="19.4" y2="13.6" />
|
||||
<path d="M15.5 31.5h15.8a5.7 5.7 0 1 0-.5-11.3 8.4 8.4 0 0 0-15.3-1.7 6.2 6.2 0 0 0 0 13Z" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'cloudy'">
|
||||
<path d="M13 31.5h18.8a6.3 6.3 0 1 0-.6-12.5 9.4 9.4 0 0 0-17.2-2.1A7 7 0 0 0 13 31.5Z" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'fog'">
|
||||
<path d="M13 25.5h18.8a6.3 6.3 0 1 0-.6-12.5 9.4 9.4 0 0 0-17.2-2.1A7 7 0 0 0 13 25.5Z" />
|
||||
<path d="M11 31c2 0 3-1 5-1s3 1 5 1 3-1 5-1 3 1 5 1 3-1 5-1" />
|
||||
<path d="M14 36c1.7 0 2.6-.8 4.3-.8s2.6.8 4.3.8 2.6-.8 4.3-.8 2.6.8 4.3.8" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'rain'">
|
||||
<path d="M13 26.5h18.8a6.3 6.3 0 1 0-.6-12.5 9.4 9.4 0 0 0-17.2-2.1A7 7 0 0 0 13 26.5Z" />
|
||||
<path d="M16 31c0 1.9-1.2 3.4-2.7 3.4s-2.7-1.5-2.7-3.4c0-1.6 2.7-4.9 2.7-4.9S16 29.4 16 31Z" />
|
||||
<path d="M25.4 34.2c0 1.8-1.1 3.2-2.4 3.2s-2.4-1.4-2.4-3.2c0-1.5 2.4-4.5 2.4-4.5s2.4 3 2.4 4.5Z" />
|
||||
<path d="M34.8 31c0 1.9-1.2 3.4-2.7 3.4s-2.7-1.5-2.7-3.4c0-1.6 2.7-4.9 2.7-4.9s2.7 3.3 2.7 4.9Z" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'snow'">
|
||||
<path d="M13 26.5h18.8a6.3 6.3 0 1 0-.6-12.5 9.4 9.4 0 0 0-17.2-2.1A7 7 0 0 0 13 26.5Z" />
|
||||
<g>
|
||||
<line x1="16.5" y1="30.5" x2="16.5" y2="38.5" />
|
||||
<line x1="13" y1="34.5" x2="20" y2="34.5" />
|
||||
<line x1="13.8" y1="31.8" x2="19.2" y2="37.2" />
|
||||
<line x1="19.2" y1="31.8" x2="13.8" y2="37.2" />
|
||||
</g>
|
||||
<g>
|
||||
<line x1="31.5" y1="30.5" x2="31.5" y2="38.5" />
|
||||
<line x1="28" y1="34.5" x2="35" y2="34.5" />
|
||||
<line x1="28.8" y1="31.8" x2="34.2" y2="37.2" />
|
||||
<line x1="34.2" y1="31.8" x2="28.8" y2="37.2" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'storm'">
|
||||
<path d="M13 26.5h18.8a6.3 6.3 0 1 0-.6-12.5 9.4 9.4 0 0 0-17.2-2.1A7 7 0 0 0 13 26.5Z" />
|
||||
<path d="M24.5 29.5l-4 7h4.4l-2 8 7.6-10h-4.8l2.3-5Z" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'sunrise'">
|
||||
<line x1="10" y1="33" x2="38" y2="33" />
|
||||
<path d="M16 33a8 8 0 0 1 16 0" />
|
||||
<line x1="24" y1="10" x2="24" y2="16" />
|
||||
<polyline points="21,13 24,10 27,13" />
|
||||
<line x1="15" y1="26" x2="17.5" y2="23.5" />
|
||||
<line x1="33" y1="26" x2="30.5" y2="23.5" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'sunset'">
|
||||
<line x1="10" y1="33" x2="38" y2="33" />
|
||||
<path d="M16 33a8 8 0 0 1 16 0" />
|
||||
<line x1="24" y1="10" x2="24" y2="16" />
|
||||
<polyline points="21,13 24,16 27,13" />
|
||||
<line x1="15" y1="26" x2="17.5" y2="28.5" />
|
||||
<line x1="33" y1="26" x2="30.5" y2="28.5" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'air'">
|
||||
<path d="M10 17c4.2 0 4.2-3 8.4-3s4.2 3 8.4 3 4.2-3 8.4-3" />
|
||||
<path d="M13 24c3.8 0 3.8-2.5 7.6-2.5s3.8 2.5 7.6 2.5 3.8-2.5 7.6-2.5" />
|
||||
<path d="M10 31c4.2 0 4.2-3 8.4-3s4.2 3 8.4 3 4.2-3 8.4-3" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'visibility'">
|
||||
<path d="M6 24c3.1-4.8 8.8-8 18-8s14.9 3.2 18 8c-3.1 4.8-8.8 8-18 8S9.1 28.8 6 24Z" />
|
||||
<circle cx="24" cy="24" r="4.8" />
|
||||
<circle cx="24" cy="24" r="1.5" fill="currentColor" stroke="none" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'humidity'">
|
||||
<path d="M24 10C20.5 15 16 20.4 16 27a8 8 0 0 0 16 0c0-6.6-4.5-12-8-17Z" />
|
||||
<path d="M24 30.5a4.6 4.6 0 0 0 4.4-3.1" />
|
||||
</g>
|
||||
|
||||
<g v-else-if="props.kind === 'wind'">
|
||||
<path d="M10 17h17c3.3 0 5.5-2 5.5-4.8 0-2.4-1.9-4.2-4.4-4.2-2.1 0-3.7 1.2-4.4 3" />
|
||||
<path d="M10 25h22c2.8 0 4.8 1.8 4.8 4.2 0 2.3-1.8 4.1-4.1 4.1-1.9 0-3.4-.9-4.2-2.4" />
|
||||
<path d="M10 33h11c2.5 0 4.3 1.6 4.3 3.8 0 2.1-1.7 3.7-3.9 3.7-1.6 0-2.9-.7-3.7-2" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.glyph {
|
||||
width: 1.12rem;
|
||||
height: 1.12rem;
|
||||
flex-shrink: 0;
|
||||
color: currentColor;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.glyph--large {
|
||||
width: 2.7rem;
|
||||
height: 2.7rem;
|
||||
}
|
||||
</style>
|
||||
11
calendar/src/env.d.ts
vendored
Normal file
11
calendar/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
|
||||
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module '*.md?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
132
calendar/src/lib/calendar.ts
Normal file
132
calendar/src/lib/calendar.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { HolidayUtil, Solar } from 'lunar-typescript';
|
||||
|
||||
export interface CalendarBadge {
|
||||
label: string;
|
||||
tone: 'holiday' | 'workday' | 'festival' | 'term';
|
||||
}
|
||||
|
||||
export interface CalendarCell {
|
||||
date: Date;
|
||||
day: number;
|
||||
subLabel: string;
|
||||
currentMonth: boolean;
|
||||
isToday: boolean;
|
||||
badges: CalendarBadge[];
|
||||
}
|
||||
|
||||
export interface CalendarModel {
|
||||
largeDay: string;
|
||||
gregorianLabel: string;
|
||||
weekdayLabel: string;
|
||||
lunarYearLabel: string;
|
||||
lunarDayLabel: string;
|
||||
summaryBadges: CalendarBadge[];
|
||||
weekLabels: string[];
|
||||
cells: CalendarCell[];
|
||||
}
|
||||
|
||||
const WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
|
||||
function isSameDay(left: Date, right: Date) {
|
||||
return (
|
||||
left.getFullYear() === right.getFullYear() &&
|
||||
left.getMonth() === right.getMonth() &&
|
||||
left.getDate() === right.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
function badgeFromHoliday(dateKey: string): CalendarBadge[] {
|
||||
const holiday = HolidayUtil.getHoliday(dateKey);
|
||||
|
||||
if (!holiday) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: holiday.getName(),
|
||||
tone: holiday.isWork() ? 'workday' : 'holiday',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildSolar(date: Date) {
|
||||
return Solar.fromYmd(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
}
|
||||
|
||||
function buildCellBadges(date: Date) {
|
||||
const solar = buildSolar(date);
|
||||
const lunar = solar.getLunar();
|
||||
const solarFestivals = solar.getFestivals();
|
||||
const lunarFestivals = lunar.getFestivals();
|
||||
const otherFestivals = solar.getOtherFestivals();
|
||||
const jieQi = lunar.getJieQi();
|
||||
|
||||
return [
|
||||
...badgeFromHoliday(solar.toYmd()),
|
||||
...solarFestivals.map((label) => ({ label, tone: 'festival' as const })),
|
||||
...lunarFestivals.map((label) => ({ label, tone: 'festival' as const })),
|
||||
...otherFestivals.map((label) => ({ label, tone: 'festival' as const })),
|
||||
...(jieQi ? [{ label: jieQi, tone: 'term' as const }] : []),
|
||||
];
|
||||
}
|
||||
|
||||
function buildCellSubLabel(date: Date) {
|
||||
const solar = buildSolar(date);
|
||||
const lunar = solar.getLunar();
|
||||
const badges = buildCellBadges(date);
|
||||
|
||||
if (badges.length > 0) {
|
||||
return badges[0].label;
|
||||
}
|
||||
|
||||
const lunarDay = lunar.getDayInChinese();
|
||||
|
||||
if (lunarDay === '初一') {
|
||||
return lunar.getMonthInChinese();
|
||||
}
|
||||
|
||||
return lunarDay;
|
||||
}
|
||||
|
||||
export function buildCalendarModel(currentDate: Date): CalendarModel {
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth();
|
||||
const monthStart = new Date(currentYear, currentMonth, 1);
|
||||
const monthEnd = new Date(currentYear, currentMonth + 1, 0);
|
||||
const gridStart = new Date(monthStart);
|
||||
gridStart.setDate(monthStart.getDate() - monthStart.getDay());
|
||||
const gridEnd = new Date(monthEnd);
|
||||
gridEnd.setDate(monthEnd.getDate() + (6 - monthEnd.getDay()));
|
||||
|
||||
const cells: CalendarCell[] = [];
|
||||
const cursor = new Date(gridStart);
|
||||
|
||||
while (cursor <= gridEnd) {
|
||||
const cellDate = new Date(cursor);
|
||||
cells.push({
|
||||
date: cellDate,
|
||||
day: cellDate.getDate(),
|
||||
subLabel: buildCellSubLabel(cellDate),
|
||||
badges: buildCellBadges(cellDate),
|
||||
currentMonth: cellDate.getMonth() === currentMonth,
|
||||
isToday: isSameDay(cellDate, currentDate),
|
||||
});
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
|
||||
const solar = buildSolar(currentDate);
|
||||
const lunar = solar.getLunar();
|
||||
const summaryBadges = buildCellBadges(currentDate).slice(0, 3);
|
||||
|
||||
return {
|
||||
largeDay: String(currentDate.getDate()).padStart(2, '0'),
|
||||
gregorianLabel: `${currentYear}年${currentMonth + 1}月${currentDate.getDate()}日`,
|
||||
weekdayLabel: `星期${solar.getWeekInChinese()}`,
|
||||
lunarYearLabel: `${lunar.getYearInGanZhi()}${lunar.getYearShengXiao()}年`,
|
||||
lunarDayLabel: `${lunar.getMonthInChinese()}${lunar.getDayInChinese()}`,
|
||||
summaryBadges,
|
||||
weekLabels: WEEK_LABELS,
|
||||
cells,
|
||||
};
|
||||
}
|
||||
49
calendar/src/lib/quotes.ts
Normal file
49
calendar/src/lib/quotes.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import quoteMarkdown from '../../../日知录.md?raw';
|
||||
|
||||
export interface QuoteEntry {
|
||||
date: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const QUOTE_PATTERN = /^>\s*(.+?)\s*--\s*(\d{4}-\d{2}-\d{2})$/gm;
|
||||
|
||||
const QUOTE_ENTRIES: QuoteEntry[] = Array.from(quoteMarkdown.matchAll(QUOTE_PATTERN)).map(
|
||||
(match) => ({
|
||||
text: match[1].trim(),
|
||||
date: match[2],
|
||||
}),
|
||||
);
|
||||
|
||||
function pad(value: number) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function toMonthDay(date: Date) {
|
||||
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
function toIsoDate(date: Date) {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
export function getQuoteForDate(date: Date) {
|
||||
const isoDate = toIsoDate(date);
|
||||
const monthDay = toMonthDay(date);
|
||||
const exactMatch = QUOTE_ENTRIES.find((entry) => entry.date === isoDate);
|
||||
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
const monthDayMatch = QUOTE_ENTRIES.find((entry) => entry.date.slice(5) === monthDay);
|
||||
|
||||
if (monthDayMatch) {
|
||||
return monthDayMatch;
|
||||
}
|
||||
|
||||
// 文件日期不覆盖全年时,回退到一个稳定的循环选择,保证每天都有内容可显示。
|
||||
const startOfYear = new Date(date.getFullYear(), 0, 1);
|
||||
const dayIndex = Math.floor((date.getTime() - startOfYear.getTime()) / 86_400_000);
|
||||
|
||||
return QUOTE_ENTRIES[(dayIndex % QUOTE_ENTRIES.length + QUOTE_ENTRIES.length) % QUOTE_ENTRIES.length];
|
||||
}
|
||||
203
calendar/src/lib/weather.ts
Normal file
203
calendar/src/lib/weather.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
export interface LocationCoordinates {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ForecastDay {
|
||||
label: string;
|
||||
weatherCode: number;
|
||||
condition: string;
|
||||
high: number;
|
||||
low: number;
|
||||
}
|
||||
|
||||
export interface WeatherSnapshot {
|
||||
temperature: number;
|
||||
condition: string;
|
||||
weatherCode: number;
|
||||
humidity: number;
|
||||
windSpeed: number;
|
||||
visibilityKm: number;
|
||||
forecast: ForecastDay[];
|
||||
sunrise: string;
|
||||
sunset: string;
|
||||
aqi: number | null;
|
||||
aqiLabel: string;
|
||||
}
|
||||
|
||||
const DEFAULT_LOCATION: LocationCoordinates = {
|
||||
latitude: 31.2304,
|
||||
longitude: 121.4737,
|
||||
label: '上海',
|
||||
};
|
||||
|
||||
const WEATHER_CODE_LABELS: Record<number, string> = {
|
||||
0: '晴朗',
|
||||
1: '晴间多云',
|
||||
2: '多云',
|
||||
3: '阴天',
|
||||
45: '有雾',
|
||||
48: '冻雾',
|
||||
51: '毛毛雨',
|
||||
53: '小雨',
|
||||
55: '中雨',
|
||||
56: '冻毛雨',
|
||||
57: '冻毛雨',
|
||||
61: '小雨',
|
||||
63: '中雨',
|
||||
65: '大雨',
|
||||
66: '冻雨',
|
||||
67: '冻雨',
|
||||
71: '小雪',
|
||||
73: '中雪',
|
||||
75: '大雪',
|
||||
77: '阵雪',
|
||||
80: '阵雨',
|
||||
81: '强阵雨',
|
||||
82: '暴雨',
|
||||
85: '阵雪',
|
||||
86: '大阵雪',
|
||||
95: '雷阵雨',
|
||||
96: '雷暴冰雹',
|
||||
99: '强雷暴',
|
||||
};
|
||||
|
||||
function formatClock(value: string) {
|
||||
const date = new Date(value);
|
||||
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatForecastLabel(value: string, index: number) {
|
||||
if (index === 0) {
|
||||
return '今天';
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
return '明天';
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
weekday: 'short',
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
function aqiToLabel(aqi: number | null) {
|
||||
if (aqi === null) {
|
||||
return '暂无';
|
||||
}
|
||||
|
||||
if (aqi <= 50) {
|
||||
return '优';
|
||||
}
|
||||
|
||||
if (aqi <= 100) {
|
||||
return '良';
|
||||
}
|
||||
|
||||
if (aqi <= 150) {
|
||||
return '轻度';
|
||||
}
|
||||
|
||||
if (aqi <= 200) {
|
||||
return '中度';
|
||||
}
|
||||
|
||||
if (aqi <= 300) {
|
||||
return '重度';
|
||||
}
|
||||
|
||||
return '严重';
|
||||
}
|
||||
|
||||
export function weatherCodeToLabel(code: number) {
|
||||
return WEATHER_CODE_LABELS[code] ?? '天气';
|
||||
}
|
||||
|
||||
export async function resolveLocation(): Promise<LocationCoordinates> {
|
||||
if (!('geolocation' in navigator)) {
|
||||
return DEFAULT_LOCATION;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
resolve({
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
label: '当前位置',
|
||||
});
|
||||
},
|
||||
() => resolve(DEFAULT_LOCATION),
|
||||
{
|
||||
enableHighAccuracy: false,
|
||||
timeout: 5000,
|
||||
maximumAge: 10 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchWeather(location: LocationCoordinates): Promise<WeatherSnapshot> {
|
||||
const weatherParams = new URLSearchParams({
|
||||
latitude: String(location.latitude),
|
||||
longitude: String(location.longitude),
|
||||
current: [
|
||||
'temperature_2m',
|
||||
'relative_humidity_2m',
|
||||
'weather_code',
|
||||
'wind_speed_10m',
|
||||
'visibility',
|
||||
].join(','),
|
||||
daily: ['weather_code', 'temperature_2m_max', 'temperature_2m_min', 'sunrise', 'sunset'].join(','),
|
||||
forecast_days: '5',
|
||||
timezone: 'auto',
|
||||
});
|
||||
|
||||
const airParams = new URLSearchParams({
|
||||
latitude: String(location.latitude),
|
||||
longitude: String(location.longitude),
|
||||
current: 'us_aqi',
|
||||
timezone: 'auto',
|
||||
});
|
||||
|
||||
const [weatherResponse, airResponse] = await Promise.all([
|
||||
fetch(`https://api.open-meteo.com/v1/forecast?${weatherParams.toString()}`),
|
||||
fetch(`https://air-quality-api.open-meteo.com/v1/air-quality?${airParams.toString()}`),
|
||||
]);
|
||||
|
||||
if (!weatherResponse.ok) {
|
||||
throw new Error(`天气接口返回异常:${weatherResponse.status}`);
|
||||
}
|
||||
|
||||
const weatherJson = await weatherResponse.json();
|
||||
const airJson = airResponse.ok ? await airResponse.json() : null;
|
||||
|
||||
const aqi = airJson?.current?.us_aqi ?? null;
|
||||
const daily = weatherJson.daily;
|
||||
|
||||
return {
|
||||
temperature: Math.round(weatherJson.current.temperature_2m),
|
||||
condition: weatherCodeToLabel(weatherJson.current.weather_code),
|
||||
weatherCode: weatherJson.current.weather_code,
|
||||
humidity: Math.round(weatherJson.current.relative_humidity_2m),
|
||||
windSpeed: Math.round(weatherJson.current.wind_speed_10m),
|
||||
visibilityKm: Number((weatherJson.current.visibility / 1000).toFixed(1)),
|
||||
forecast: daily.time.map((value: string, index: number) => ({
|
||||
label: formatForecastLabel(value, index),
|
||||
weatherCode: daily.weather_code[index],
|
||||
condition: weatherCodeToLabel(daily.weather_code[index]),
|
||||
high: Math.round(daily.temperature_2m_max[index]),
|
||||
low: Math.round(daily.temperature_2m_min[index]),
|
||||
})),
|
||||
sunrise: formatClock(daily.sunrise[0]),
|
||||
sunset: formatClock(daily.sunset[0]),
|
||||
aqi,
|
||||
aqiLabel: aqiToLabel(aqi),
|
||||
};
|
||||
}
|
||||
6
calendar/src/main.ts
Normal file
6
calendar/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import App from './App.vue';
|
||||
import './style.css';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
448
calendar/src/style.css
Normal file
448
calendar/src/style.css
Normal file
@@ -0,0 +1,448 @@
|
||||
:root {
|
||||
font-family:
|
||||
'Hiragino Sans GB',
|
||||
'PingFang SC',
|
||||
'Noto Sans SC',
|
||||
sans-serif;
|
||||
color: #0f172a;
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, rgba(255, 255, 255, 0.88), transparent 30%),
|
||||
radial-gradient(circle at 82% 18%, rgba(241, 238, 232, 0.92), transparent 24%),
|
||||
linear-gradient(160deg, #f4f1eb 0%, #ebe5dc 52%, #e4ddd4 100%);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-frame {
|
||||
width: min(100%, 1448px);
|
||||
aspect-ratio: 1448 / 1072;
|
||||
padding: clamp(1.2rem, 1vw + 0.9rem, 2rem);
|
||||
border-radius: 2rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0.18)),
|
||||
rgba(255, 255, 255, 0.14);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.55),
|
||||
0 38px 72px rgba(64, 52, 38, 0.14);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.08fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: clamp(1rem, 1vw + 0.75rem, 2rem);
|
||||
height: 100%;
|
||||
align-content: start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 1.6rem;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow:
|
||||
0 20px 36px rgba(72, 58, 44, 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
border: 1px solid rgba(70, 58, 46, 0.08);
|
||||
}
|
||||
|
||||
.calendar-card,
|
||||
.weather-card {
|
||||
padding: clamp(1.4rem, 1.2vw + 1rem, 2rem);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.calendar-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.calendar-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.calendar-card__day {
|
||||
font-family:
|
||||
'Iowan Old Style',
|
||||
'Baskerville',
|
||||
serif;
|
||||
font-size: clamp(4.4rem, 5vw, 7rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.08em;
|
||||
color: #101010;
|
||||
}
|
||||
|
||||
.calendar-card__meta {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.calendar-card__date-line,
|
||||
.calendar-card__lunar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
margin: 0;
|
||||
font-size: clamp(1rem, 0.7vw + 0.7rem, 1.55rem);
|
||||
}
|
||||
|
||||
.calendar-card__weekday {
|
||||
color: #556274;
|
||||
}
|
||||
|
||||
.calendar-card__lunar {
|
||||
color: #5f4b32;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.calendar-card__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.calendar-card__badges--inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
|
||||
.calendar-card__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 1.6rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid currentColor;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.calendar-card__badge--holiday {
|
||||
color: #2f2b26;
|
||||
background: #ece5db;
|
||||
}
|
||||
|
||||
.calendar-card__badge--workday {
|
||||
color: #68593f;
|
||||
background: #f2eadb;
|
||||
}
|
||||
|
||||
.calendar-card__badge--festival {
|
||||
color: #4f4436;
|
||||
background: #f5f0e8;
|
||||
}
|
||||
|
||||
.calendar-card__badge--term {
|
||||
color: #3a4a4a;
|
||||
background: #e9efef;
|
||||
}
|
||||
|
||||
.calendar-card__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
grid-template-rows: auto;
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
gap: 0.35rem 0.3rem;
|
||||
align-content: start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.calendar-card__week-label {
|
||||
text-align: center;
|
||||
color: #798395;
|
||||
font-size: 0.92rem;
|
||||
padding-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.calendar-card__cell {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
padding: 0.2rem 0;
|
||||
border-radius: 0.8rem;
|
||||
color: #304153;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.calendar-card__cell--muted {
|
||||
color: #bac4d2;
|
||||
}
|
||||
|
||||
.calendar-card__cell--today {
|
||||
background: linear-gradient(180deg, #303640 0%, #1f252d 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 14px 24px rgba(31, 37, 45, 0.24);
|
||||
}
|
||||
|
||||
.calendar-card__cell--holiday .calendar-card__solar,
|
||||
.calendar-card__cell--holiday .calendar-card__lunar-day {
|
||||
color: #6a4a1c;
|
||||
}
|
||||
|
||||
.calendar-card__solar {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.calendar-card__lunar-day {
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.2;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.weather-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.weather-card__heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.weather-card__title {
|
||||
margin: 0;
|
||||
font-size: clamp(1.5rem, 1vw + 1rem, 2rem);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.weather-card__subtitle {
|
||||
margin: 0.35rem 0 0;
|
||||
color: #778396;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.weather-card__hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.05rem 1.2rem;
|
||||
border-radius: 1.2rem;
|
||||
background: linear-gradient(180deg, #ece8e1 0%, #e2ddd4 100%);
|
||||
}
|
||||
|
||||
.weather-card__hero--placeholder {
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.weather-card__hero-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.weather-card__temperature {
|
||||
font-size: clamp(2.2rem, 1.2vw + 1.6rem, 3.2rem);
|
||||
line-height: 1;
|
||||
font-family:
|
||||
'Iowan Old Style',
|
||||
'Baskerville',
|
||||
serif;
|
||||
}
|
||||
|
||||
.weather-card__condition {
|
||||
margin-top: 0.35rem;
|
||||
font-size: 1.15rem;
|
||||
color: #243041;
|
||||
}
|
||||
|
||||
.weather-card__facts {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.weather-card__fact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.weather-card__forecast {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.forecast-pill {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.8rem 0.55rem;
|
||||
border-radius: 1rem;
|
||||
background: #f7f4ef;
|
||||
border: 1px solid rgba(82, 70, 56, 0.08);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.forecast-pill__label {
|
||||
margin: 0;
|
||||
color: #556274;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.forecast-pill__temp {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.forecast-pill__temp span {
|
||||
margin-left: 0.15rem;
|
||||
color: #7d8798;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.weather-card__metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.metric-pill {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.85rem 0.9rem 0.9rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.metric-pill__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.metric-pill__value {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric--sunrise {
|
||||
background: linear-gradient(150deg, #f3eee5, #ebe3d6);
|
||||
color: #5c4631;
|
||||
}
|
||||
|
||||
.metric--sunset {
|
||||
background: linear-gradient(150deg, #efebe6, #e5dfd6);
|
||||
color: #4f463d;
|
||||
}
|
||||
|
||||
.metric--air {
|
||||
background: linear-gradient(150deg, #eef0eb, #e3e7de);
|
||||
color: #354338;
|
||||
}
|
||||
|
||||
.metric--visibility {
|
||||
background: linear-gradient(150deg, #eef1f0, #e2e5e4);
|
||||
color: #3b4a4d;
|
||||
}
|
||||
|
||||
.quote-card {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 1.1rem;
|
||||
padding: clamp(1.5rem, 1.2vw + 1rem, 2rem);
|
||||
background: linear-gradient(168deg, rgba(249, 245, 236, 0.98), rgba(244, 239, 229, 0.94));
|
||||
}
|
||||
|
||||
.quote-card__content {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
line-height: 1.65;
|
||||
font-family:
|
||||
'Iowan Old Style',
|
||||
'Baskerville',
|
||||
'Noto Serif SC',
|
||||
serif;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.page-shell {
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.dashboard-frame {
|
||||
aspect-ratio: auto;
|
||||
width: min(100%, 1448px);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
.quote-card {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.weather-card__forecast,
|
||||
.weather-card__metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.calendar-card__header,
|
||||
.weather-card__heading,
|
||||
.weather-card__hero {
|
||||
grid-template-columns: 1fr;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.calendar-card__header {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.weather-card__forecast,
|
||||
.weather-card__metrics {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.quote-card__content {
|
||||
font-size: 1.45rem !important;
|
||||
}
|
||||
}
|
||||
21
calendar/tsconfig.json
Normal file
21
calendar/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
19
calendar/vite.config.ts
Normal file
19
calendar/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
// 允许直接读取仓库根目录下的《日知录》原始 Markdown。
|
||||
allow: ['..'],
|
||||
},
|
||||
},
|
||||
});
|
||||
22
dash/.github/workflows/ci.yml
vendored
Normal file
22
dash/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
sh-checker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run sh-checker
|
||||
uses: luizm/action-sh-checker@v0.1.12
|
||||
env:
|
||||
SHELLCHECK_OPTS: -s ash
|
||||
SHFMT_OPTS: -i 2
|
||||
with:
|
||||
sh_checker_comment: false
|
||||
3
dash/.gitignore
vendored
Normal file
3
dash/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/dist
|
||||
/kindle-dash-*.tgz
|
||||
/tmp
|
||||
43
dash/CHANGELOG.md
Normal file
43
dash/CHANGELOG.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v1.0.0-beta.4] - 2022-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Only call eips if fetch-dashboard succesfully completes
|
||||
- Ensure a full screen refresh is triggered after wake from sleep
|
||||
- Build ht from upstream sources, using rusttls instead of vendored openssl
|
||||
- Replace ht 0.4.0 with xh 0.16.1 (project was renamed)
|
||||
|
||||
## [v1.0.0-beta.3] - 2020-02-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Use 1.1.1.1 as default Wi-Fi test ip
|
||||
- Use a more standards-compliant cron parser (BREAKING)
|
||||
|
||||
### Added
|
||||
|
||||
- Add low battery reporting (`local/low-battery.sh`)
|
||||
- Add debug mode (DEBUG=true start.sh)
|
||||
- SSH server prerequisite in docs (@julianlam)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Typos (@jcmiller11, @starcoat)
|
||||
|
||||
## [v1.0.0-beta-2] - 2020-01-26
|
||||
|
||||
### Removed
|
||||
|
||||
- Power state logging
|
||||
|
||||
## [v1.0.0-beta-1] - 2020-01-26
|
||||
|
||||
Initial release 🎉
|
||||
10
dash/KUAL/kindle-dash/config.xml
Normal file
10
dash/KUAL/kindle-dash/config.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension>
|
||||
<information>
|
||||
<name>Kindle dashboard</name>
|
||||
<id>pascalw-kindle-dash</id>
|
||||
</information>
|
||||
<menus>
|
||||
<menu type="json" dynamic="true">menu.json</menu>
|
||||
</menus>
|
||||
</extension>
|
||||
5
dash/KUAL/kindle-dash/menu.json
Normal file
5
dash/KUAL/kindle-dash/menu.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"items": [
|
||||
{"name": "Kindle Dashboard", "action": "/mnt/us/dashboard/start.sh"}
|
||||
]
|
||||
}
|
||||
19
dash/LICENSE
Normal file
19
dash/LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2021 Pascal Widdershoven
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
44
dash/Makefile
Normal file
44
dash/Makefile
Normal file
@@ -0,0 +1,44 @@
|
||||
VERSION := v1.0.0-beta.4
|
||||
SRC_FILES := $(shell find src -name '*.sh' -o -name '*.png')
|
||||
NEXT_WAKEUP_SRC_FILES := $(shell find src/next-wakeup/src -name '*.rs')
|
||||
TARGET_FILES := $(SRC_FILES:src/%=dist/%)
|
||||
|
||||
dist: dist/next-wakeup dist/xh dist/local/state ${TARGET_FILES}
|
||||
|
||||
tarball: dist
|
||||
tar -C dist -cvzf kindle-dash-${VERSION}.tgz ./
|
||||
|
||||
dist/%: src/%
|
||||
@echo "Copying $<"
|
||||
@mkdir -p $(@D)
|
||||
@cp "$<" "$@"
|
||||
|
||||
dist/next-wakeup: ${NEXT_WAKEUP_SRC_FILES}
|
||||
cd src/next-wakeup && cross build --release --target arm-unknown-linux-musleabi
|
||||
cp src/next-wakeup/target/arm-unknown-linux-musleabi/release/next-wakeup dist/
|
||||
|
||||
dist/xh: tmp/xh
|
||||
cd tmp/xh && cross build --release --target arm-unknown-linux-musleabi
|
||||
docker run --rm \
|
||||
-v $(shell pwd)/tmp/xh:/src \
|
||||
rustembedded/cross:arm-unknown-linux-musleabi-0.2.1 \
|
||||
/usr/local/arm-linux-musleabi/bin/strip /src/target/arm-unknown-linux-musleabi/release/xh
|
||||
cp tmp/xh/target/arm-unknown-linux-musleabi/release/xh dist/
|
||||
|
||||
tmp/xh:
|
||||
mkdir -p tmp/
|
||||
git clone --depth 1 --branch v0.16.1 https://github.com/ducaale/xh.git tmp/xh
|
||||
|
||||
dist/local/state:
|
||||
mkdir -p dist/local/state
|
||||
|
||||
clean:
|
||||
rm -r dist/*
|
||||
|
||||
watch:
|
||||
watchexec -w src/ -p -- make
|
||||
|
||||
format:
|
||||
shfmt -i 2 -w -l src/**/*.sh
|
||||
|
||||
.PHONY: clean watch tarball format
|
||||
59
dash/README.md
Normal file
59
dash/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Low-power Kindle dashboard
|
||||
|
||||
Turns out old Kindle devices make great, energy efficient dashboards :-)
|
||||
|
||||

|
||||
|
||||
## What this repo is
|
||||
|
||||
This repo only contains the code that runs on the Kindle. It periodically fetches an image to be displayed on the screen and suspends the device to RAM (which is very power efficient) until the next screen update.
|
||||
|
||||
This code _does not_ render the dashboard itself. It's expected that what to display on the screen is rendered elsewhere and can be fetchd via HTTP(s). This is both more power efficient and allows you to use any tool you like to produce the dashboard image.
|
||||
|
||||
In my case I use a [dashbling](https://github.com/pascalw/dashbling) dashboard that I render into a PNG screenshot on a server. See [here](https://github.com/pascalw/kindle-dash/blob/main/docs/tipstricks.md#producing-dashboard-images-from-a-webpage) for information on how these PNGs should be produced, including some sample code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* A jailbroken Kindle, with Wi-Fi configured.
|
||||
* An SSH server on the Kindle (via [USBNetwork](https://wiki.mobileread.com/wiki/USBNetwork))
|
||||
* Tested only on a Kindle 4 NT. Should work on other Kindle devices as well with minor modifications.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the [latest release](https://github.com/pascalw/kindle-dash/releases) on your computer and extract it.
|
||||
2. Modify `local/fetch-dashboard.sh` and optionally `local/env.sh`.
|
||||
3. Copy the files to the Kindle, for example: `rsync -vr ./ kindle:/mnt/us/dashboard`.
|
||||
4. Start dashboard with `/mnt/us/dashboard/start.sh`.
|
||||
Note that the device will go into suspend about 10-15 seconds after you start the dashboard.
|
||||
|
||||
## Upgrading
|
||||
|
||||
If you're running kindle-dash already and want to update to the latest version follow the following steps.
|
||||
|
||||
1. Download the [latest release](https://github.com/pascalw/kindle-dash/releases) on your computer and extract it.
|
||||
2. Review the release notes. Some releases might require changes to files in `local/`.
|
||||
3. Copy the files to the Kindle, excluding the `local` directory. For example: `rsync -vur --exclude=local ./ kindle:/mnt/us/dashboard`.
|
||||
4. Modify files in `/mnt/us/dashboard/local` if applicable.
|
||||
5. Start dashboard with `/mnt/us/dashboard/start.sh`.
|
||||
Note that the device will go into suspend about 10-15 seconds after you start the dashboard.
|
||||
|
||||
## KUAL
|
||||
|
||||
If you're using KUAL you can use simple extension to start this Dashboard
|
||||
|
||||
1. Copy folder `kindle-dash` from `KUAL` folder to the kual `extensions` folder. (located in `/mnt/us/extensions`)
|
||||
|
||||
## How this works
|
||||
|
||||
* This code periodically downloads a dashboard image from an HTTP(s) endpoint.
|
||||
* The interval can be configured in `dist/local/env.sh` using a cron expression.
|
||||
* During the update intervals the device is suspended to RAM to save power.
|
||||
|
||||
## Notes
|
||||
|
||||
* The releases contain a pre-compiled binary of the [ht](https://github.com/ducaale/ht) command-line HTTP client. This fully supports modern HTTPS crypto, wheras the built-in `curl` and `wget` commands don't (because they rely on a very old `openssl` library).
|
||||
* For a detailed Kindle Voyage 5.13.6 jailbreak and deployment walkthrough, see [docs/kindle-voyage-5.13.6-watchthis-zh.md](./docs/kindle-voyage-5.13.6-watchthis-zh.md).
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to [davidhampgonsalves/life-dashboard](https://github.com/davidhampgonsalves/life-dashboard) for the inspiration!
|
||||
238
dash/docs/kindle-voyage-5.13.6-watchthis-zh.md
Normal file
238
dash/docs/kindle-voyage-5.13.6-watchthis-zh.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Kindle Voyage 5.13.6 一次成功路径
|
||||
|
||||
这篇文档只覆盖下面这个组合:
|
||||
|
||||
- 机型:`Kindle Voyage (KV)`
|
||||
- 固件:`5.13.6`
|
||||
- 目标:完成越狱,并部署 `KUAL`、`MRPI`、`renameotabin` 和 `kindle-dash`
|
||||
|
||||
如果设备型号或固件版本不同,不要直接照抄本文。
|
||||
|
||||
## 核心结论
|
||||
|
||||
`Kindle Voyage 5.13.6` 应该走 `WatchThis`,不要走 `LanguageBreak`。
|
||||
|
||||
这次实操里,前面大部分失败都来自两个错误:
|
||||
|
||||
- 误走了 `LanguageBreak`
|
||||
- 在 demo 菜单里点错了分支,提前进入了 `Resell Device` / `销售设备`
|
||||
|
||||
对这台设备,正确思路非常简单:
|
||||
|
||||
1. 用 `WatchThis` 进入 demo mode
|
||||
2. 只在正确的 `Sideload Content` 时机导入 `KV-5.13.6.zip`
|
||||
3. 用 `Get Started` 触发越狱脚本
|
||||
4. 安装 `KUAL/MRPI`
|
||||
5. 用 `renameotabin` 关闭 OTA
|
||||
6. 再部署并启动 `kindle-dash`
|
||||
|
||||
## 需要准备的文件
|
||||
|
||||
### WatchThis
|
||||
|
||||
来自 `watchthis-jailbreak-r03.zip`:
|
||||
|
||||
- `KV-5.13.6.zip`
|
||||
- `demo.json`
|
||||
- `Update_hotfix_watchthis_custom.bin`
|
||||
|
||||
在本仓库里对应的是:
|
||||
|
||||
- `staging/watchthis/KV-5.13.6/KV-5.13.6.zip`
|
||||
- `staging/watchthis/KV-5.13.6/demo.json`
|
||||
- `staging/watchthis/Update_hotfix_watchthis_custom.bin`
|
||||
|
||||
### 越狱后安装包
|
||||
|
||||
- `extensions/MRInstaller`
|
||||
- `mrpackages/Update_KUALBooklet_HDRepack.bin`
|
||||
- `extensions/renameotabin`
|
||||
- `extensions/kindle-dash`
|
||||
- `dashboard/`
|
||||
|
||||
在本仓库里已经整理到:
|
||||
|
||||
- `staging/post-jailbreak-root/extensions/`
|
||||
- `staging/post-jailbreak-root/mrpackages/`
|
||||
- `staging/post-jailbreak-root/dashboard/`
|
||||
|
||||
## 一次成功的正确路径
|
||||
|
||||
### 1. 恢复出厂并进入 demo mode
|
||||
|
||||
1. 先恢复出厂设置。
|
||||
2. 语言选择页只选 `English (United Kingdom)`。
|
||||
这一步非常关键,不要选中文。
|
||||
3. 到 Wi‑Fi 页面后,随便点一个网络,再立刻退回,不要真的联网。
|
||||
4. 在搜索栏输入 `;enter_demo`。
|
||||
5. 如果 `;enter_demo` 没反应,走备用入口:
|
||||
- 用 USB 连接电脑
|
||||
- 在 Kindle 根目录创建空文件 `DONT_CHECK_BATTERY`
|
||||
- 弹出设备
|
||||
- 回到 Kindle 搜索输入 `;demo`
|
||||
6. 如果看到 `Demo Activation`,点 `Yes`。
|
||||
7. 设备重启并进入 demo 流程后:
|
||||
- 跳过 Wi‑Fi
|
||||
- 店铺注册信息全部填假值
|
||||
- `Fetching available demo types` 选 `Skip`
|
||||
- demo type 选 `standard`
|
||||
|
||||
### 2. 第一次出现 Sideload Content 时不要导入 payload
|
||||
|
||||
1. 第一次出现 `Add Content` / `Sideload Content` 提示时,只点 `Done`。
|
||||
2. 这一步不要接 USB。
|
||||
3. 这一步也不要导入 `KV-5.13.6.zip`。
|
||||
|
||||
这是最容易做错的一步。第一次 `Done` 只是让 demo setup 继续往下走,不是真正的 payload 导入点。
|
||||
|
||||
### 3. 跳过 misconfiguration 锁页
|
||||
|
||||
demo setup 完成后,大概率会落到 `Configure Device` / misconfiguration 页面。
|
||||
|
||||
不要点 `Configure Device`,直接做隐藏手势:
|
||||
|
||||
1. 在屏幕右下角用两根手指同时轻点一下
|
||||
2. 两指立刻抬起
|
||||
3. 马上用一根手指从右下向左滑
|
||||
|
||||
触发成功后会回到可操作界面。
|
||||
|
||||
### 4. 真正的 payload 导入点
|
||||
|
||||
1. 回到可操作界面后,搜索输入 `;demo`
|
||||
2. 进入 demo menu
|
||||
3. 选择 `Sideload Content` / `导入内容`
|
||||
4. 到这一步再接 USB
|
||||
5. 在 Kindle 根目录创建 `.demo/`
|
||||
6. 把下面三个东西放进去:
|
||||
|
||||
```text
|
||||
.demo/KV-5.13.6.zip
|
||||
.demo/demo.json
|
||||
.demo/goodreads/ <- 空目录
|
||||
```
|
||||
|
||||
如果你在 Mac 上操作,可以直接用:
|
||||
|
||||
```sh
|
||||
mkdir -p /Volumes/Kindle/.demo/goodreads
|
||||
cp staging/watchthis/KV-5.13.6/KV-5.13.6.zip /Volumes/Kindle/.demo/
|
||||
cp staging/watchthis/KV-5.13.6/demo.json /Volumes/Kindle/.demo/
|
||||
```
|
||||
|
||||
然后:
|
||||
|
||||
1. 弹出 Kindle
|
||||
2. 在 Kindle 上点 `Done`
|
||||
3. 退出 demo menu
|
||||
|
||||
## 5. 触发越狱脚本
|
||||
|
||||
1. 退出 demo menu 后,输入 `;dsts`
|
||||
如果 `;dsts` 没反应,也可以从顶部下拉进入设置。
|
||||
2. 打开 `Help & User Guides`
|
||||
3. 再点 `Get Started`
|
||||
4. 设备会重启
|
||||
5. 越狱脚本会在下次启动时运行
|
||||
|
||||
如果这里弹 `Application Error`,官方补救是:
|
||||
|
||||
1. 长按电源键强制重启
|
||||
2. 再进 demo menu
|
||||
3. 再执行一次 `Sideload Content -> Done`
|
||||
4. 这次不要再接 USB
|
||||
|
||||
## 6. 成功判据
|
||||
|
||||
对这台设备,下面这些现象说明越狱已经落地:
|
||||
|
||||
- Kindle 用户存储根目录出现 `mkk`
|
||||
- Kindle 用户存储根目录出现 `libkh`
|
||||
- Kindle 用户存储根目录出现 `rp`
|
||||
|
||||
如果这三个目录都没有,基本就是前面的 `WatchThis` 没真正成功。
|
||||
|
||||
## 7. 安装 KUAL / MRPI / kindle-dash
|
||||
|
||||
越狱落地后,把这些目录复制到 Kindle:
|
||||
|
||||
```sh
|
||||
rsync -a staging/post-jailbreak-root/extensions/ /Volumes/Kindle/extensions/
|
||||
rsync -a staging/post-jailbreak-root/mrpackages/ /Volumes/Kindle/mrpackages/
|
||||
rsync -a staging/post-jailbreak-root/dashboard/ /Volumes/Kindle/dashboard/
|
||||
```
|
||||
|
||||
然后:
|
||||
|
||||
1. 弹出 Kindle
|
||||
2. 回到首页搜索输入 `;log mrpi`
|
||||
3. 等安装完成
|
||||
4. 首页会出现 `KUAL` 卡片
|
||||
|
||||
## 8. 启动顺序
|
||||
|
||||
进入 `KUAL` 后,先做这个顺序:
|
||||
|
||||
1. `Rename OTA Binaries -> Rename`
|
||||
2. 再运行 `Kindle Dashboard`
|
||||
|
||||
不要先跑 `Kindle Dashboard`,否则后面如果 OTA 没关掉,还存在自动升级把越狱覆盖掉的风险。
|
||||
|
||||
## 9. kindle-dash 默认行为
|
||||
|
||||
本项目默认不会在 Kindle 本机实时渲染页面,而是定时去下载一张图片来显示。
|
||||
|
||||
因此:
|
||||
|
||||
- 如果没有联网,`Kindle Dashboard` 看起来会像“卡住”
|
||||
- 如果刷新计划不覆盖当前时间,会显示 `kindle is sleeping`
|
||||
- 图片最好直接按 Voyage 原生分辨率出图:`1072 x 1448`
|
||||
|
||||
默认抓图脚本在:
|
||||
|
||||
- `src/local/fetch-dashboard.sh`
|
||||
|
||||
默认刷新计划在:
|
||||
|
||||
- `src/local/env.sh`
|
||||
|
||||
## 10. 这台设备上确认过的坑
|
||||
|
||||
### 不要走 `LanguageBreak`
|
||||
|
||||
`KV + 5.13.6` 应走 `WatchThis`。误走 `LanguageBreak` 会导致:
|
||||
|
||||
- `;demo -> Yes -> 重启 -> 回普通系统`
|
||||
- `;uzb`、`;dsts` 行为异常
|
||||
- 反复进入错误的 demo 分支
|
||||
|
||||
### 不要点 `Resell Device` / `销售设备`
|
||||
|
||||
这个分支会把流程带到 shipping mode / demo 出厂流程,和 `WatchThis` 正常路径无关。
|
||||
|
||||
如果你是在 `WatchThis` 流程里,demo menu 里真正要点的是:
|
||||
|
||||
- `Sideload Content`
|
||||
|
||||
不是:
|
||||
|
||||
- `Resell Device`
|
||||
- `Remote Reset`
|
||||
- `Configure WiFi`
|
||||
|
||||
### 第一次 `Add Content` 只能点 `Done`
|
||||
|
||||
真正要接 USB 导 payload 的时机,是秘密手势之后再次 `;demo -> Sideload Content` 的那一次。
|
||||
|
||||
### 看到左上角只有一小块图片,不一定是失败
|
||||
|
||||
这通常只是图片尺寸不匹配。
|
||||
|
||||
例如本项目自带的 `sleeping.png` 只有 `600x800`,放到 Voyage 上就只会显示在左上角一部分区域。
|
||||
|
||||
## 参考
|
||||
|
||||
- WatchThis 包内说明:`watchthis-jailbreak-r03.zip` 中的 `watchthis-release/README.md`
|
||||
- 项目主说明:`README.md`
|
||||
- 图片抓取说明:`src/local/fetch-dashboard.sh`
|
||||
- 本地调度配置:`src/local/env.sh`
|
||||
449
dash/docs/layered-clock-plan.zh.md
Normal file
449
dash/docs/layered-clock-plan.zh.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# Kindle Dashboard 分层时钟方案
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前仓库里有两部分:
|
||||
|
||||
- `calendar/`:负责渲染仪表盘网页与导出背景素材
|
||||
- `dash/`:运行在 Kindle 上,负责拉图、刷屏、休眠与唤醒
|
||||
|
||||
最新设计稿来自 Figma:
|
||||
|
||||
- 文件:`calendar`
|
||||
- 节点:`6:2`
|
||||
- 链接:`https://www.figma.com/design/3bXFNM5nM6mCq0TpL3nPYK/calendar?node-id=6-2&m=dev`
|
||||
|
||||
该节点 annotation 已明确:
|
||||
|
||||
- `阳历当天`
|
||||
- `星期`
|
||||
- `农历日`
|
||||
- `时钟区域`
|
||||
- `日历`
|
||||
- `天气预报卡片`
|
||||
- `书摘卡片`
|
||||
|
||||
本方案基于这些约束,采用“低频背景 + 本地时钟”的拆分方式:
|
||||
|
||||
- `calendar/` 每 `2 小时` 生成一次背景图
|
||||
- 背景图直接写到:
|
||||
- `/Users/gavin/kindle-dash/calendar/dist/kindlebg.png`
|
||||
- Kindle 通过固定 HTTPS 地址拉取背景图:
|
||||
- `https://shell.biboer.cn:20001/kindlebg.png`
|
||||
- Kindle 端每分钟`不联网`
|
||||
- Kindle 端只在本地重画时钟区域
|
||||
|
||||
## 2. 目标
|
||||
|
||||
这次改造不是单纯调样式,而是把页面拆成两类刷新节奏完全不同的素材:
|
||||
|
||||
1. 低频背景层
|
||||
2. 高频时钟层
|
||||
|
||||
目标收益:
|
||||
|
||||
- 背景内容不再分钟级刷新
|
||||
- 分钟级变化只限制在时钟区域
|
||||
- 降低 Wi-Fi 唤醒次数
|
||||
- 降低全屏刷新频率
|
||||
- 减少墨水屏闪烁与残影
|
||||
|
||||
## 3. 分层边界
|
||||
|
||||
### 3.1 全屏背景层
|
||||
|
||||
背景层文件名固定为:
|
||||
|
||||
- `kindlebg.png`
|
||||
|
||||
背景层包含:
|
||||
|
||||
- 整体外层容器、圆角、阴影、背景渐变
|
||||
- 日历卡片中的:
|
||||
- 阳历当天数字
|
||||
- 星期
|
||||
- 农历日
|
||||
- 下方月历区域
|
||||
- 天气卡片全部内容
|
||||
- 书摘卡片全部内容
|
||||
|
||||
背景层**不包含**:
|
||||
|
||||
- 时钟表盘主体
|
||||
- 时钟刻度
|
||||
- 时针
|
||||
- 分针
|
||||
- 中心圆点
|
||||
|
||||
也就是说,背景图在时钟区域只保留布局占位,不直接承载任何分钟级内容。
|
||||
|
||||
### 3.2 静态表盘 patch
|
||||
|
||||
静态表盘 patch 为一个独立本地素材:
|
||||
|
||||
- `clock-face.png`
|
||||
|
||||
该素材包含:
|
||||
|
||||
- 圆形表盘主体
|
||||
- 刻度
|
||||
- 中心底座或中心圆盘
|
||||
|
||||
建议尺寸直接对齐 Figma 时钟区域:
|
||||
|
||||
- 节点:`24:74`
|
||||
- 设计尺寸:`220 x 220`
|
||||
|
||||
这张图应当保存在 Kindle 本地,例如:
|
||||
|
||||
- `/mnt/us/dashboard/assets/clock-face.png`
|
||||
|
||||
它不需要分钟级联网拉取,只需要在部署时同步到设备,或者在更换设计稿时重新同步。
|
||||
|
||||
### 3.3 指针层
|
||||
|
||||
高频变化层只包含:
|
||||
|
||||
- 时针
|
||||
- 分针
|
||||
|
||||
这里不建议包含秒针:
|
||||
|
||||
- 墨水屏收益低
|
||||
- 刷新频率会显著上升
|
||||
- 更容易产生残影
|
||||
|
||||
由于当前 `dash/` 链路本质是 `eips` 刷图,不是 SVG/Canvas 实时渲染,所以推荐使用`分层素材方案`:
|
||||
|
||||
- `minute-hand/00.png` 到 `minute-hand/59.png`
|
||||
- `hour-hand/000.png` 到 `hour-hand/719.png`
|
||||
|
||||
说明:
|
||||
|
||||
- 分针每分钟一个角度,共 `60` 张
|
||||
- 时针如果要做到真正随分钟连续移动,需要 `12 * 60 = 720` 张
|
||||
- 这些都是小尺寸 patch,不是整屏图,体积可控
|
||||
|
||||
如果后续确认 Kindle 端存在稳定的本地绘线工具,再考虑把指针改成算法绘制;当前版本不依赖这个前提。
|
||||
|
||||
## 4. 为什么不能只把表盘放进整页背景
|
||||
|
||||
这个问题必须单独说明。
|
||||
|
||||
如果:
|
||||
|
||||
- 表盘主体和刻度只存在于 `kindlebg.png`
|
||||
- Kindle 每分钟只覆盖新的时针/分针
|
||||
|
||||
那么上一分钟留下的旧指针就无法被干净擦除。
|
||||
|
||||
因此 Kindle 端每分钟的正确流程应该是:
|
||||
|
||||
1. 先重画一张本地 `clock-face.png`
|
||||
2. 再叠加新的时针素材
|
||||
3. 再叠加新的分针素材
|
||||
|
||||
这等价于“先擦除,再重画”,并且整个流程不依赖网络。
|
||||
|
||||
所以本方案不是“背景图 + 两根指针”两层,而是:
|
||||
|
||||
1. `kindlebg.png`:全屏低频背景
|
||||
2. `clock-face.png`:本地静态表盘 patch
|
||||
3. `hour-hand/*.png + minute-hand/*.png`:本地高频指针素材
|
||||
|
||||
## 5. 数据刷新策略
|
||||
|
||||
### 5.1 背景层刷新
|
||||
|
||||
背景层刷新触发条件:
|
||||
|
||||
- 每 `2 小时` 一次
|
||||
- 跨天时立即刷新一次
|
||||
- 天气接口异常恢复后可补刷一次
|
||||
|
||||
推荐调度:
|
||||
|
||||
- `00:00 / 02:00 / 04:00 / ... / 22:00`
|
||||
|
||||
背景层刷新输出:
|
||||
|
||||
- `/Users/gavin/kindle-dash/calendar/dist/kindlebg.png`
|
||||
|
||||
背景层对 Kindle 的访问地址固定为:
|
||||
|
||||
- `https://shell.biboer.cn:20001/kindlebg.png`
|
||||
|
||||
### 5.2 静态表盘 patch 刷新
|
||||
|
||||
静态表盘 patch 不参加分钟级调度。
|
||||
|
||||
建议刷新方式:
|
||||
|
||||
- 随部署同步一次
|
||||
- Figma 设计或尺寸变化时重新导出并同步
|
||||
|
||||
### 5.3 指针层刷新
|
||||
|
||||
指针层刷新触发条件:
|
||||
|
||||
- 每分钟一次
|
||||
|
||||
指针层数据只依赖 Kindle 本地时间:
|
||||
|
||||
- 小时
|
||||
- 分钟
|
||||
|
||||
分钟刷新不需要联网。
|
||||
|
||||
## 6. 网页渲染模式设计
|
||||
|
||||
为了让 `calendar/` 稳定产出背景与表盘素材,建议支持下面 3 种模式。
|
||||
|
||||
### 6.1 `full`
|
||||
|
||||
用途:
|
||||
|
||||
- 本地开发预览
|
||||
- 对照 Figma 联调
|
||||
|
||||
输出内容:
|
||||
|
||||
- 背景层
|
||||
- 表盘
|
||||
- 指针预览
|
||||
|
||||
### 6.2 `background`
|
||||
|
||||
用途:
|
||||
|
||||
- 生成 `kindlebg.png`
|
||||
|
||||
输出内容:
|
||||
|
||||
- 只渲染背景层
|
||||
- 时钟区域保留占位,但不绘制表盘与指针
|
||||
|
||||
### 6.3 `clock-face`
|
||||
|
||||
用途:
|
||||
|
||||
- 生成静态表盘 patch
|
||||
|
||||
输出内容:
|
||||
|
||||
- 只渲染表盘主体、刻度和中心底座
|
||||
- 不绘制时针、分针
|
||||
|
||||
### 6.4 URL 约定
|
||||
|
||||
建议页面支持如下参数:
|
||||
|
||||
```text
|
||||
/?mode=full
|
||||
/?mode=background
|
||||
/?mode=clock-face
|
||||
```
|
||||
|
||||
## 7. 产物约定
|
||||
|
||||
推荐最终产出这几类文件:
|
||||
|
||||
```text
|
||||
calendar/dist/kindlebg.png
|
||||
calendar/dist/dashboard-manifest.json
|
||||
|
||||
kindle local:
|
||||
/mnt/us/dashboard/assets/clock-face.png
|
||||
/mnt/us/dashboard/assets/hour-hand/000.png ... 719.png
|
||||
/mnt/us/dashboard/assets/minute-hand/00.png ... 59.png
|
||||
```
|
||||
|
||||
`manifest` 建议至少包含这些字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"background": {
|
||||
"path": "kindlebg.png",
|
||||
"url": "https://shell.biboer.cn:20001/kindlebg.png",
|
||||
"updatedAt": "2026-03-15T10:00:00+08:00",
|
||||
"refreshIntervalMinutes": 120
|
||||
},
|
||||
"clockRegion": {
|
||||
"x": 313,
|
||||
"y": 0,
|
||||
"width": 220,
|
||||
"height": 220
|
||||
},
|
||||
"clockFace": {
|
||||
"path": "clock-face.png",
|
||||
"managedOnKindle": true
|
||||
},
|
||||
"clockHands": {
|
||||
"hourPattern": "assets/hour-hand/%03d.png",
|
||||
"minutePattern": "assets/minute-hand/%02d.png",
|
||||
"refreshIntervalMinutes": 1,
|
||||
"networkRequired": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `x/y/width/height` 先按设计稿记录
|
||||
- 真正接入 Kindle 时,要换算成最终截图分辨率下的实际像素值
|
||||
|
||||
## 8. Kindle 侧刷新策略
|
||||
|
||||
### 8.1 已确认的前提
|
||||
|
||||
`eips` 支持把 PNG/JPG 绘制到指定坐标,参数包含:
|
||||
|
||||
- `-g`
|
||||
- `-x`
|
||||
- `-y`
|
||||
- `-f`
|
||||
|
||||
MobileRead Wiki 明确写了:
|
||||
|
||||
- `eips -g|-b image_path [-w waveform -f -x xpos -y ypos -v]`
|
||||
- `-x` 与 `-y` 以像素为单位
|
||||
来源:<https://wiki.mobileread.com/wiki/Eips>
|
||||
|
||||
因此,对 Kindle Voyage 而言,“在固定时钟区域重画小图”这个前提是成立的。
|
||||
|
||||
### 8.2 推荐流程
|
||||
|
||||
#### 启动或背景刷新时
|
||||
|
||||
1. 通过 HTTPS 拉取:
|
||||
- `https://shell.biboer.cn:20001/kindlebg.png`
|
||||
2. 保存为本地背景缓存
|
||||
3. 使用全屏刷新显示背景图
|
||||
4. 在时钟区域重画一次:
|
||||
- `clock-face.png`
|
||||
- 当前时针
|
||||
- 当前分针
|
||||
|
||||
#### 每分钟刷新时
|
||||
|
||||
1. 读取 Kindle 本机时间
|
||||
2. 计算:
|
||||
- `minute_index = 00..59`
|
||||
- `hour_index = ((hour % 12) * 60 + minute) = 000..719`
|
||||
3. 在固定坐标先画:
|
||||
- `clock-face.png`
|
||||
4. 再画:
|
||||
- `hour-hand/<hour_index>.png`
|
||||
5. 再画:
|
||||
- `minute-hand/<minute_index>.png`
|
||||
6. 默认做局部/普通刷新
|
||||
7. 每 `10` 到 `15` 分钟对时钟区域补一次全刷,清理残影
|
||||
|
||||
### 8.3 功耗模型
|
||||
|
||||
这套方式的功耗来源拆成两类:
|
||||
|
||||
- 背景刷新:
|
||||
- 每 2 小时联网一次
|
||||
- 全屏刷新一次
|
||||
- 时钟刷新:
|
||||
- 每分钟本地刷一次小区域
|
||||
- 不联网
|
||||
|
||||
相比“每分钟拉一张整屏背景图”,这会明显省电。
|
||||
|
||||
## 9. 代码改造建议
|
||||
|
||||
### 9.1 `calendar/`
|
||||
|
||||
建议改造点:
|
||||
|
||||
- 新增 `mode=background`
|
||||
- 新增 `mode=clock-face`
|
||||
- 时钟区域从日历/天气/书摘中完全拆开
|
||||
- 增加一个定时生成任务,每 2 小时把背景图写到:
|
||||
- `/Users/gavin/kindle-dash/calendar/dist/kindlebg.png`
|
||||
- 生成 `dashboard-manifest.json`
|
||||
|
||||
### 9.2 `dash/`
|
||||
|
||||
建议改造点:
|
||||
|
||||
- `dash/src/local/fetch-dashboard.sh`
|
||||
- 改成只拉取 `kindlebg.png`
|
||||
- `dash/src/dash.sh`
|
||||
- 从“单一刷新循环”改为“背景刷新 + 本地时钟刷新”双节奏
|
||||
- 新增例如:
|
||||
- `dash/src/local/render-clock.sh`
|
||||
- `dash/src/local/render-clock-face.sh`
|
||||
- `dash/src/local/clock-index.sh`
|
||||
|
||||
推荐新增配置项:
|
||||
|
||||
```sh
|
||||
export BACKGROUND_URL="https://shell.biboer.cn:20001/kindlebg.png"
|
||||
export BACKGROUND_REFRESH_SCHEDULE="0 */2 * * *"
|
||||
export CLOCK_REGION_X=313
|
||||
export CLOCK_REGION_Y=0
|
||||
export CLOCK_REGION_WIDTH=220
|
||||
export CLOCK_REGION_HEIGHT=220
|
||||
export CLOCK_FULL_REFRESH_INTERVAL_MINUTES=15
|
||||
```
|
||||
|
||||
## 10. 实施顺序
|
||||
|
||||
建议按下面顺序落地,避免一次改太多导致链路难排查。
|
||||
|
||||
### 阶段 1:`calendar/` 分层输出
|
||||
|
||||
目标:
|
||||
|
||||
- `full/background/clock-face` 三种模式跑通
|
||||
- 每 2 小时把背景图写到 `calendar/dist/kindlebg.png`
|
||||
|
||||
输出:
|
||||
|
||||
- 浏览器可预览
|
||||
- `kindlebg.png` 可被 nginx 直接访问
|
||||
|
||||
### 阶段 2:时钟静态素材准备
|
||||
|
||||
目标:
|
||||
|
||||
- 产出 `clock-face.png`
|
||||
- 产出 `hour-hand` 与 `minute-hand` 素材库
|
||||
|
||||
输出:
|
||||
|
||||
- Kindle 本地时钟素材目录结构确定
|
||||
|
||||
### 阶段 3:Kindle 本地分钟时钟
|
||||
|
||||
目标:
|
||||
|
||||
- Kindle 每分钟本地重画时钟
|
||||
- 不联网
|
||||
|
||||
输出:
|
||||
|
||||
- 背景低频更新 + 时钟本地高频更新的完整链路
|
||||
|
||||
## 11. 推荐结论
|
||||
|
||||
当前最稳妥的推进顺序是:
|
||||
|
||||
1. 先让 `calendar/` 每 2 小时稳定生成:
|
||||
- `/Users/gavin/kindle-dash/calendar/dist/kindlebg.png`
|
||||
2. 再让 Kindle 只从:
|
||||
- `https://shell.biboer.cn:20001/kindlebg.png`
|
||||
拉背景图
|
||||
3. 时钟区域完全本地化:
|
||||
- 本地 `clock-face.png`
|
||||
- 本地 `hour-hand/*.png`
|
||||
- 本地 `minute-hand/*.png`
|
||||
4. 分钟刷新时:
|
||||
- 先重画表盘
|
||||
- 再重画时针
|
||||
- 再重画分针
|
||||
|
||||
也就是说,**背景是远端低频资源,时钟是本地高频资源,二者不要混在同一个刷新链路里**。
|
||||
1
dash/docs/screenshotter/.dockerignore
Normal file
1
dash/docs/screenshotter/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
/node_modules
|
||||
1
dash/docs/screenshotter/.gitignore
vendored
Normal file
1
dash/docs/screenshotter/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/node_modules
|
||||
30
dash/docs/screenshotter/Dockerfile
Normal file
30
dash/docs/screenshotter/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
FROM node:12-buster-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y wget gnupg \
|
||||
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
||||
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y google-chrome-stable fonts-freefont-ttf libxss1 fonts-noto-color-emoji \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD 1
|
||||
ENV PUPPETEER_EXECUTABLE_PATH /usr/bin/google-chrome-stable
|
||||
|
||||
ADD package.json yarn.lock /app/
|
||||
|
||||
RUN cd /app && yarn install \
|
||||
# Add user so we don't need --no-sandbox.
|
||||
# same layer as npm install to keep re-chowned files from using up several hundred MBs more space
|
||||
&& groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
|
||||
&& mkdir -p /home/pptruser/Downloads \
|
||||
&& chown -R pptruser:pptruser /home/pptruser \
|
||||
&& chown -R pptruser:pptruser /app/node_modules
|
||||
|
||||
ADD . /app/
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
USER pptruser
|
||||
|
||||
CMD ["node", "/app/screenshot.js"]
|
||||
9
dash/docs/screenshotter/package.json
Normal file
9
dash/docs/screenshotter/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "screenshotter",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"pngjs": "^6.0.0",
|
||||
"puppeteer": "^5.5.0"
|
||||
}
|
||||
}
|
||||
38
dash/docs/screenshotter/screenshot.js
Normal file
38
dash/docs/screenshotter/screenshot.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const puppeteer = require("puppeteer");
|
||||
const fs = require("fs");
|
||||
const PNG = require("pngjs").PNG;
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const URL = process.env.URL || "CHANGEME";
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
/* Kindle 4 NT resolution */
|
||||
await page.setViewport({ width: 600, height: 800 });
|
||||
|
||||
/* Might want to use networkidle0 here, depending on the type of page */
|
||||
/* See https://github.com/puppeteer/puppeteer/blob/main/docs/api.md */
|
||||
await page.goto(URL, { waitUntil: "networkidle2" });
|
||||
|
||||
/* This is a bit silly. ¯\_(ツ)_/¯
|
||||
Networkidle2 doesn't always seem to wait long enough. */
|
||||
await sleep(3000);
|
||||
|
||||
await page.screenshot({ path: "dash.png" });
|
||||
|
||||
await fs.createReadStream("dash.png")
|
||||
.pipe(new PNG({ colorType: 0 }))
|
||||
.on("parsed", function () {
|
||||
this.pack().pipe(fs.createWriteStream("dash.png"));
|
||||
});
|
||||
|
||||
browser.close();
|
||||
})();
|
||||
373
dash/docs/screenshotter/yarn.lock
Normal file
373
dash/docs/screenshotter/yarn.lock
Normal file
@@ -0,0 +1,373 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@*":
|
||||
version "14.14.22"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18"
|
||||
integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"
|
||||
integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
agent-base@5:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
|
||||
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
bl@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
|
||||
integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==
|
||||
dependencies:
|
||||
buffer "^5.5.0"
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
|
||||
|
||||
buffer@^5.2.1, buffer@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
chownr@^1.1.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
devtools-protocol@0.0.818844:
|
||||
version "0.0.818844"
|
||||
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e"
|
||||
integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==
|
||||
|
||||
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
extract-zip@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
|
||||
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
get-stream "^5.1.0"
|
||||
yauzl "^2.10.0"
|
||||
optionalDependencies:
|
||||
"@types/yauzl" "^2.9.1"
|
||||
|
||||
fd-slicer@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
|
||||
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
find-up@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
||||
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
||||
dependencies:
|
||||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
get-stream@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
|
||||
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
https-proxy-agent@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b"
|
||||
integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==
|
||||
dependencies:
|
||||
agent-base "5"
|
||||
debug "4"
|
||||
|
||||
ieee754@^1.1.13:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
locate-path@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
|
||||
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
|
||||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
mkdirp-classic@^0.5.2:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
p-limit@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
|
||||
dependencies:
|
||||
p-try "^2.0.0"
|
||||
|
||||
p-locate@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
|
||||
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
|
||||
dependencies:
|
||||
p-limit "^2.2.0"
|
||||
|
||||
p-try@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
pend@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
|
||||
|
||||
pkg-dir@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
|
||||
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
|
||||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
pngjs@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
|
||||
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
|
||||
|
||||
progress@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||
|
||||
proxy-from-env@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
puppeteer@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.5.0.tgz#331a7edd212ca06b4a556156435f58cbae08af00"
|
||||
integrity sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==
|
||||
dependencies:
|
||||
debug "^4.1.0"
|
||||
devtools-protocol "0.0.818844"
|
||||
extract-zip "^2.0.0"
|
||||
https-proxy-agent "^4.0.0"
|
||||
node-fetch "^2.6.1"
|
||||
pkg-dir "^4.2.0"
|
||||
progress "^2.0.1"
|
||||
proxy-from-env "^1.0.0"
|
||||
rimraf "^3.0.2"
|
||||
tar-fs "^2.0.0"
|
||||
unbzip2-stream "^1.3.3"
|
||||
ws "^7.2.3"
|
||||
|
||||
readable-stream@^3.1.1, readable-stream@^3.4.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
rimraf@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
tar-fs@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
|
||||
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
|
||||
dependencies:
|
||||
chownr "^1.1.1"
|
||||
mkdirp-classic "^0.5.2"
|
||||
pump "^3.0.0"
|
||||
tar-stream "^2.1.4"
|
||||
|
||||
tar-stream@^2.1.4:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
|
||||
dependencies:
|
||||
bl "^4.0.3"
|
||||
end-of-stream "^1.4.1"
|
||||
fs-constants "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
through@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
unbzip2-stream@^1.3.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
|
||||
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
|
||||
dependencies:
|
||||
buffer "^5.2.1"
|
||||
through "^2.3.8"
|
||||
|
||||
util-deprecate@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
ws@^7.2.3:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
|
||||
integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
|
||||
dependencies:
|
||||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
12
dash/docs/tipstricks.md
Normal file
12
dash/docs/tipstricks.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Tips & tricks
|
||||
|
||||
## Producing dashboard images from a webpage
|
||||
|
||||
A common way to produce dashboard images for the Kindle is to take a screenshot of a website.
|
||||
This can be done in a variety of ways. A few things to keep in mind are:
|
||||
|
||||
1. The images should be grayscale PNG images, without any alpha layers.
|
||||
2. The resolution should match the display resolution of the Kindle. For example the Kindle 4 NT has a resolution of 800x600 pixels.
|
||||
|
||||
I personally use a headless Chrome instance with [Puppeteer](https://pptr.dev/).
|
||||
The code I use can be found [here](./screenshotter/screenshot.js) as a reference.
|
||||
BIN
dash/example/example.png
Normal file
BIN
dash/example/example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
dash/example/photo.jpg
Normal file
BIN
dash/example/photo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 458 KiB |
122
dash/src/dash.sh
Executable file
122
dash/src/dash.sh
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
DASH_PNG="$DIR/dash.png"
|
||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||
LOW_BATTERY_CMD="$DIR/local/low-battery.sh"
|
||||
|
||||
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600}
|
||||
RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||
|
||||
LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||
|
||||
num_refresh=0
|
||||
|
||||
init() {
|
||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
||||
echo "Missing required configuration."
|
||||
echo "Timezone: ${TIMEZONE:-(not set)}."
|
||||
echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
||||
|
||||
/etc/init.d/framework stop
|
||||
initctl stop webreader >/dev/null 2>&1
|
||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
||||
}
|
||||
|
||||
prepare_sleep() {
|
||||
echo "Preparing sleep"
|
||||
|
||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||
|
||||
# Give screen time to refresh
|
||||
sleep 2
|
||||
|
||||
# Ensure a full screen refresh is triggered after wake from sleep
|
||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||
}
|
||||
|
||||
refresh_dashboard() {
|
||||
echo "Refreshing dashboard"
|
||||
"$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP"
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$DASH_PNG"
|
||||
fetch_status=$?
|
||||
|
||||
if [ "$fetch_status" -ne 0 ]; then
|
||||
echo "Not updating screen, fetch-dashboard returned $fetch_status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then
|
||||
num_refresh=0
|
||||
|
||||
# trigger a full refresh once in every 4 refreshes, to keep the screen clean
|
||||
echo "Full screen refresh"
|
||||
/usr/sbin/eips -f -g "$DASH_PNG"
|
||||
else
|
||||
echo "Partial screen refresh"
|
||||
/usr/sbin/eips -g "$DASH_PNG"
|
||||
fi
|
||||
|
||||
num_refresh=$((num_refresh + 1))
|
||||
}
|
||||
|
||||
log_battery_stats() {
|
||||
battery_level=$(gasgauge-info -c)
|
||||
echo "$(date) Battery level: $battery_level."
|
||||
|
||||
if [ "$LOW_BATTERY_REPORTING" = true ]; then
|
||||
battery_level_numeric=${battery_level%?}
|
||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
rtc_sleep() {
|
||||
duration=$1
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
sleep "$duration"
|
||||
else
|
||||
# shellcheck disable=SC2039
|
||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
||||
echo "mem" >/sys/power/state
|
||||
fi
|
||||
}
|
||||
|
||||
main_loop() {
|
||||
while true; do
|
||||
log_battery_stats
|
||||
|
||||
next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE")
|
||||
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then
|
||||
action="sleep"
|
||||
prepare_sleep
|
||||
else
|
||||
action="suspend"
|
||||
refresh_dashboard
|
||||
fi
|
||||
|
||||
# take a bit of time before going to sleep, so this process can be aborted
|
||||
sleep 10
|
||||
|
||||
echo "Going to $action, next wakeup in ${next_wakeup_secs}s"
|
||||
|
||||
rtc_sleep "$next_wakeup_secs"
|
||||
done
|
||||
}
|
||||
|
||||
init
|
||||
main_loop
|
||||
23
dash/src/local/env.sh
Normal file
23
dash/src/local/env.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Export environment variables here
|
||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||
|
||||
# By default, partial screen updates are used to update the screen,
|
||||
# to prevent the screen from flashing. After a few partial updates,
|
||||
# the screen will start to look a bit distorted (due to e-ink ghosting).
|
||||
# 测试阶段强制每次都做一次全刷,避免首页残影和局部刷新的旧内容干扰验证。
|
||||
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
||||
export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
|
||||
# When the time until the next wakeup is greater or equal to this number,
|
||||
# the dashboard will not be refreshed anymore, but instead show a
|
||||
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
||||
# during the day, for example.
|
||||
export SLEEP_SCREEN_INTERVAL=3600
|
||||
|
||||
export LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
export LOW_BATTERY_THRESHOLD_PERCENT=10
|
||||
4
dash/src/local/fetch-dashboard.sh
Executable file
4
dash/src/local/fetch-dashboard.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
# Fetch a new dashboard image, make sure to output it to "$1".
|
||||
# For example:
|
||||
"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png
|
||||
17
dash/src/local/low-battery.sh
Normal file
17
dash/src/local/low-battery.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env sh
|
||||
battery_level_percentage=$1
|
||||
last_battery_report_state="$(dirname "$0")/state/last_battery_report"
|
||||
|
||||
previous_report_timestamp=$(cat "$last_battery_report_state" 2>/dev/null || echo '-1')
|
||||
now=$(date +%s)
|
||||
|
||||
# Implement desired logic here. The example below for example only reports low
|
||||
# battery every 24 hours.
|
||||
|
||||
if [ "$previous_report_timestamp" -eq -1 ] ||
|
||||
[ $((now - previous_report_timestamp)) -gt 86400 ]; then
|
||||
# Replace this with for example an HTTP call via curl, or xh
|
||||
echo "Reporting low battery: $battery_level_percentage%"
|
||||
|
||||
echo "$now" >"$last_battery_report_state"
|
||||
fi
|
||||
1
dash/src/next-wakeup/.gitignore
vendored
Normal file
1
dash/src/next-wakeup/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
143
dash/src/next-wakeup/Cargo.lock
generated
Normal file
143
dash/src/next-wakeup/Cargo.lock
generated
Normal file
@@ -0,0 +1,143 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"parse-zoneinfo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cron-parser"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8446d1ce86096fd2260e732c6f5dffc752cbfa6cb14371d057bc1de3a1831b6"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kindle-dash-next-wakeup"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"cron-parser",
|
||||
"pico-args",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pico-args"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d70072c20945e1ab871c472a285fc772aefd4f5407723c206242f2c6f94595d6"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
15
dash/src/next-wakeup/Cargo.toml
Normal file
15
dash/src/next-wakeup/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "kindle-dash-next-wakeup"
|
||||
version = "0.1.0"
|
||||
authors = ["Pascal Widdershoven <hello@pascalw.me>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
cron-parser = "0.7.7"
|
||||
chrono = "0.4.19"
|
||||
chrono-tz = "0.5.3"
|
||||
pico-args = "0.4.0"
|
||||
|
||||
[[bin]]
|
||||
name = "next-wakeup"
|
||||
path = "src/main.rs"
|
||||
54
dash/src/next-wakeup/src/main.rs
Normal file
54
dash/src/next-wakeup/src/main.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use chrono::Utc;
|
||||
use chrono_tz::Tz;
|
||||
|
||||
const HELP: &str = "\
|
||||
USAGE:
|
||||
next-wakeup --schedule '2,32 8-17 * * MON-FRI' --timezone 'Europe/Amsterdam'
|
||||
next-wakeup -s='2,32 8-17 * * MON-FRI' -tz='Europe/Amsterdam'
|
||||
|
||||
OPTIONS:
|
||||
-tz, --timezone STRING Timezone used to interpret the cron schedule
|
||||
-s, --schedule STRING Cron schedule to calculate next wakeup
|
||||
-h, --help Prints help information
|
||||
";
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Args {
|
||||
timezone: Tz,
|
||||
schedule: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = match parse_args() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}.", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let schedule = args.schedule;
|
||||
|
||||
let now = Utc::now().with_timezone(&args.timezone);
|
||||
let next = cron_parser::parse(&schedule, &now).expect("Invalid cron schedule");
|
||||
|
||||
let diff = next - now;
|
||||
|
||||
println!("{}", diff.num_seconds());
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, pico_args::Error> {
|
||||
let mut pargs = pico_args::Arguments::from_env();
|
||||
|
||||
if pargs.contains(["-h", "--help"]) {
|
||||
print!("{}", HELP);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let args = Args {
|
||||
timezone: pargs.value_from_str(["-tz", "--timezone"])?,
|
||||
schedule: pargs.value_from_str(["-s", "--schedule"])?
|
||||
};
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
BIN
dash/src/sleeping.png
Normal file
BIN
dash/src/sleeping.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
18
dash/src/start.sh
Executable file
18
dash/src/start.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
LOG_FILE="$DIR/logs/dash.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
"$DIR/dash.sh"
|
||||
else
|
||||
"$DIR/dash.sh" >>"$LOG_FILE" 2>&1 &
|
||||
fi
|
||||
2
dash/src/stop.sh
Executable file
2
dash/src/stop.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
pkill -f dash.sh
|
||||
26
dash/src/wait-for-wifi.sh
Executable file
26
dash/src/wait-for-wifi.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env sh
|
||||
test_ip=$1
|
||||
|
||||
if [ -z "$test_ip" ]; then
|
||||
echo "No test ip specified"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_wifi() {
|
||||
max_retry=30
|
||||
counter=0
|
||||
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
|
||||
# shellcheck disable=SC2181
|
||||
while [ $? -ne 0 ]; do
|
||||
[ $counter -eq $max_retry ] && echo "Couldn't connect to Wi-Fi" && exit 1
|
||||
counter=$((counter + 1))
|
||||
|
||||
sleep 1
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_wifi
|
||||
echo "Wi-Fi connected"
|
||||
122
dash/staging/device/dashboard/dash.sh
Executable file
122
dash/staging/device/dashboard/dash.sh
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
DASH_PNG="$DIR/dash.png"
|
||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||
LOW_BATTERY_CMD="$DIR/local/low-battery.sh"
|
||||
|
||||
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600}
|
||||
RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||
|
||||
LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||
|
||||
num_refresh=0
|
||||
|
||||
init() {
|
||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
||||
echo "Missing required configuration."
|
||||
echo "Timezone: ${TIMEZONE:-(not set)}."
|
||||
echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
||||
|
||||
/etc/init.d/framework stop
|
||||
initctl stop webreader >/dev/null 2>&1
|
||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
||||
}
|
||||
|
||||
prepare_sleep() {
|
||||
echo "Preparing sleep"
|
||||
|
||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||
|
||||
# Give screen time to refresh
|
||||
sleep 2
|
||||
|
||||
# Ensure a full screen refresh is triggered after wake from sleep
|
||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||
}
|
||||
|
||||
refresh_dashboard() {
|
||||
echo "Refreshing dashboard"
|
||||
"$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP"
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$DASH_PNG"
|
||||
fetch_status=$?
|
||||
|
||||
if [ "$fetch_status" -ne 0 ]; then
|
||||
echo "Not updating screen, fetch-dashboard returned $fetch_status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then
|
||||
num_refresh=0
|
||||
|
||||
# trigger a full refresh once in every 4 refreshes, to keep the screen clean
|
||||
echo "Full screen refresh"
|
||||
/usr/sbin/eips -f -g "$DASH_PNG"
|
||||
else
|
||||
echo "Partial screen refresh"
|
||||
/usr/sbin/eips -g "$DASH_PNG"
|
||||
fi
|
||||
|
||||
num_refresh=$((num_refresh + 1))
|
||||
}
|
||||
|
||||
log_battery_stats() {
|
||||
battery_level=$(gasgauge-info -c)
|
||||
echo "$(date) Battery level: $battery_level."
|
||||
|
||||
if [ "$LOW_BATTERY_REPORTING" = true ]; then
|
||||
battery_level_numeric=${battery_level%?}
|
||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
rtc_sleep() {
|
||||
duration=$1
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
sleep "$duration"
|
||||
else
|
||||
# shellcheck disable=SC2039
|
||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
||||
echo "mem" >/sys/power/state
|
||||
fi
|
||||
}
|
||||
|
||||
main_loop() {
|
||||
while true; do
|
||||
log_battery_stats
|
||||
|
||||
next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE")
|
||||
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then
|
||||
action="sleep"
|
||||
prepare_sleep
|
||||
else
|
||||
action="suspend"
|
||||
refresh_dashboard
|
||||
fi
|
||||
|
||||
# take a bit of time before going to sleep, so this process can be aborted
|
||||
sleep 10
|
||||
|
||||
echo "Going to $action, next wakeup in ${next_wakeup_secs}s"
|
||||
|
||||
rtc_sleep "$next_wakeup_secs"
|
||||
done
|
||||
}
|
||||
|
||||
init
|
||||
main_loop
|
||||
23
dash/staging/device/dashboard/local/env.sh
Normal file
23
dash/staging/device/dashboard/local/env.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Export environment variables here
|
||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||
|
||||
# By default, partial screen updates are used to update the screen,
|
||||
# to prevent the screen from flashing. After a few partial updates,
|
||||
# the screen will start to look a bit distorted (due to e-ink ghosting).
|
||||
# 测试阶段强制每次都做一次全刷,避免首页残影和局部刷新的旧内容干扰验证。
|
||||
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
||||
export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
|
||||
# When the time until the next wakeup is greater or equal to this number,
|
||||
# the dashboard will not be refreshed anymore, but instead show a
|
||||
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
||||
# during the day, for example.
|
||||
export SLEEP_SCREEN_INTERVAL=3600
|
||||
|
||||
export LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
export LOW_BATTERY_THRESHOLD_PERCENT=10
|
||||
4
dash/staging/device/dashboard/local/fetch-dashboard.sh
Executable file
4
dash/staging/device/dashboard/local/fetch-dashboard.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
# Fetch a new dashboard image, make sure to output it to "$1".
|
||||
# For example:
|
||||
"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png
|
||||
17
dash/staging/device/dashboard/local/low-battery.sh
Normal file
17
dash/staging/device/dashboard/local/low-battery.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env sh
|
||||
battery_level_percentage=$1
|
||||
last_battery_report_state="$(dirname "$0")/state/last_battery_report"
|
||||
|
||||
previous_report_timestamp=$(cat "$last_battery_report_state" 2>/dev/null || echo '-1')
|
||||
now=$(date +%s)
|
||||
|
||||
# Implement desired logic here. The example below for example only reports low
|
||||
# battery every 24 hours.
|
||||
|
||||
if [ "$previous_report_timestamp" -eq -1 ] ||
|
||||
[ $((now - previous_report_timestamp)) -gt 86400 ]; then
|
||||
# Replace this with for example an HTTP call via curl, or xh
|
||||
echo "Reporting low battery: $battery_level_percentage%"
|
||||
|
||||
echo "$now" >"$last_battery_report_state"
|
||||
fi
|
||||
BIN
dash/staging/device/dashboard/next-wakeup
Executable file
BIN
dash/staging/device/dashboard/next-wakeup
Executable file
Binary file not shown.
BIN
dash/staging/device/dashboard/sleeping.png
Normal file
BIN
dash/staging/device/dashboard/sleeping.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
18
dash/staging/device/dashboard/start.sh
Executable file
18
dash/staging/device/dashboard/start.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
LOG_FILE="$DIR/logs/dash.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
"$DIR/dash.sh"
|
||||
else
|
||||
"$DIR/dash.sh" >>"$LOG_FILE" 2>&1 &
|
||||
fi
|
||||
2
dash/staging/device/dashboard/stop.sh
Executable file
2
dash/staging/device/dashboard/stop.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
pkill -f dash.sh
|
||||
26
dash/staging/device/dashboard/wait-for-wifi.sh
Executable file
26
dash/staging/device/dashboard/wait-for-wifi.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env sh
|
||||
test_ip=$1
|
||||
|
||||
if [ -z "$test_ip" ]; then
|
||||
echo "No test ip specified"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_wifi() {
|
||||
max_retry=30
|
||||
counter=0
|
||||
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
|
||||
# shellcheck disable=SC2181
|
||||
while [ $? -ne 0 ]; do
|
||||
[ $counter -eq $max_retry ] && echo "Couldn't connect to Wi-Fi" && exit 1
|
||||
counter=$((counter + 1))
|
||||
|
||||
sleep 1
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_wifi
|
||||
echo "Wi-Fi connected"
|
||||
BIN
dash/staging/device/dashboard/xh
Executable file
BIN
dash/staging/device/dashboard/xh
Executable file
Binary file not shown.
10
dash/staging/device/extensions/kindle-dash/config.xml
Normal file
10
dash/staging/device/extensions/kindle-dash/config.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension>
|
||||
<information>
|
||||
<name>Kindle dashboard</name>
|
||||
<id>pascalw-kindle-dash</id>
|
||||
</information>
|
||||
<menus>
|
||||
<menu type="json" dynamic="true">menu.json</menu>
|
||||
</menus>
|
||||
</extension>
|
||||
5
dash/staging/device/extensions/kindle-dash/menu.json
Normal file
5
dash/staging/device/extensions/kindle-dash/menu.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"items": [
|
||||
{"name": "Kindle Dashboard", "action": "/mnt/us/dashboard/start.sh"}
|
||||
]
|
||||
}
|
||||
122
dash/staging/kindle-dash-release/dash.sh
Executable file
122
dash/staging/kindle-dash-release/dash.sh
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
DASH_PNG="$DIR/dash.png"
|
||||
FETCH_DASHBOARD_CMD="$DIR/local/fetch-dashboard.sh"
|
||||
LOW_BATTERY_CMD="$DIR/local/low-battery.sh"
|
||||
|
||||
REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"2,32 8-17 * * MON-FRI"}
|
||||
FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
SLEEP_SCREEN_INTERVAL=${SLEEP_SCREEN_INTERVAL:-3600}
|
||||
RTC=/sys/devices/platform/mxc_rtc.0/wakeup_enable
|
||||
|
||||
LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
LOW_BATTERY_THRESHOLD_PERCENT=${LOW_BATTERY_THRESHOLD_PERCENT:-10}
|
||||
|
||||
num_refresh=0
|
||||
|
||||
init() {
|
||||
if [ -z "$TIMEZONE" ] || [ -z "$REFRESH_SCHEDULE" ]; then
|
||||
echo "Missing required configuration."
|
||||
echo "Timezone: ${TIMEZONE:-(not set)}."
|
||||
echo "Schedule: ${REFRESH_SCHEDULE:-(not set)}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting dashboard with $REFRESH_SCHEDULE refresh..."
|
||||
|
||||
/etc/init.d/framework stop
|
||||
initctl stop webreader >/dev/null 2>&1
|
||||
echo powersave >/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
lipc-set-prop com.lab126.powerd preventScreenSaver 1
|
||||
}
|
||||
|
||||
prepare_sleep() {
|
||||
echo "Preparing sleep"
|
||||
|
||||
/usr/sbin/eips -f -g "$DIR/sleeping.png"
|
||||
|
||||
# Give screen time to refresh
|
||||
sleep 2
|
||||
|
||||
# Ensure a full screen refresh is triggered after wake from sleep
|
||||
num_refresh=$FULL_DISPLAY_REFRESH_RATE
|
||||
}
|
||||
|
||||
refresh_dashboard() {
|
||||
echo "Refreshing dashboard"
|
||||
"$DIR/wait-for-wifi.sh" "$WIFI_TEST_IP"
|
||||
|
||||
"$FETCH_DASHBOARD_CMD" "$DASH_PNG"
|
||||
fetch_status=$?
|
||||
|
||||
if [ "$fetch_status" -ne 0 ]; then
|
||||
echo "Not updating screen, fetch-dashboard returned $fetch_status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$num_refresh" -eq "$FULL_DISPLAY_REFRESH_RATE" ]; then
|
||||
num_refresh=0
|
||||
|
||||
# trigger a full refresh once in every 4 refreshes, to keep the screen clean
|
||||
echo "Full screen refresh"
|
||||
/usr/sbin/eips -f -g "$DASH_PNG"
|
||||
else
|
||||
echo "Partial screen refresh"
|
||||
/usr/sbin/eips -g "$DASH_PNG"
|
||||
fi
|
||||
|
||||
num_refresh=$((num_refresh + 1))
|
||||
}
|
||||
|
||||
log_battery_stats() {
|
||||
battery_level=$(gasgauge-info -c)
|
||||
echo "$(date) Battery level: $battery_level."
|
||||
|
||||
if [ "$LOW_BATTERY_REPORTING" = true ]; then
|
||||
battery_level_numeric=${battery_level%?}
|
||||
if [ "$battery_level_numeric" -le "$LOW_BATTERY_THRESHOLD_PERCENT" ]; then
|
||||
"$LOW_BATTERY_CMD" "$battery_level_numeric"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
rtc_sleep() {
|
||||
duration=$1
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
sleep "$duration"
|
||||
else
|
||||
# shellcheck disable=SC2039
|
||||
[ "$(cat "$RTC")" -eq 0 ] && echo -n "$duration" >"$RTC"
|
||||
echo "mem" >/sys/power/state
|
||||
fi
|
||||
}
|
||||
|
||||
main_loop() {
|
||||
while true; do
|
||||
log_battery_stats
|
||||
|
||||
next_wakeup_secs=$("$DIR/next-wakeup" --schedule="$REFRESH_SCHEDULE" --timezone="$TIMEZONE")
|
||||
|
||||
if [ "$next_wakeup_secs" -gt "$SLEEP_SCREEN_INTERVAL" ]; then
|
||||
action="sleep"
|
||||
prepare_sleep
|
||||
else
|
||||
action="suspend"
|
||||
refresh_dashboard
|
||||
fi
|
||||
|
||||
# take a bit of time before going to sleep, so this process can be aborted
|
||||
sleep 10
|
||||
|
||||
echo "Going to $action, next wakeup in ${next_wakeup_secs}s"
|
||||
|
||||
rtc_sleep "$next_wakeup_secs"
|
||||
done
|
||||
}
|
||||
|
||||
init
|
||||
main_loop
|
||||
23
dash/staging/kindle-dash-release/local/env.sh
Normal file
23
dash/staging/kindle-dash-release/local/env.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Export environment variables here
|
||||
export WIFI_TEST_IP=${WIFI_TEST_IP:-1.1.1.1}
|
||||
# 测试配置:全天每分钟刷新一次,便于验证图片拉取与屏幕刷新是否正常。
|
||||
export REFRESH_SCHEDULE=${REFRESH_SCHEDULE:-"* * * * *"}
|
||||
export TIMEZONE=${TIMEZONE:-"Asia/Shanghai"}
|
||||
|
||||
# By default, partial screen updates are used to update the screen,
|
||||
# to prevent the screen from flashing. After a few partial updates,
|
||||
# the screen will start to look a bit distorted (due to e-ink ghosting).
|
||||
# 测试阶段强制每次都做一次全刷,避免首页残影和局部刷新的旧内容干扰验证。
|
||||
# 等图片尺寸与刷新逻辑确认无误后,再改回 4 之类的值以节省功耗。
|
||||
export FULL_DISPLAY_REFRESH_RATE=${FULL_DISPLAY_REFRESH_RATE:-0}
|
||||
|
||||
# When the time until the next wakeup is greater or equal to this number,
|
||||
# the dashboard will not be refreshed anymore, but instead show a
|
||||
# 'kindle is sleeping' screen. This can be useful if your schedule only runs
|
||||
# during the day, for example.
|
||||
export SLEEP_SCREEN_INTERVAL=3600
|
||||
|
||||
export LOW_BATTERY_REPORTING=${LOW_BATTERY_REPORTING:-false}
|
||||
export LOW_BATTERY_THRESHOLD_PERCENT=10
|
||||
4
dash/staging/kindle-dash-release/local/fetch-dashboard.sh
Executable file
4
dash/staging/kindle-dash-release/local/fetch-dashboard.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
# Fetch a new dashboard image, make sure to output it to "$1".
|
||||
# For example:
|
||||
"$(dirname "$0")/../xh" -d -q -o "$1" get https://raw.githubusercontent.com/pascalw/kindle-dash/master/example/example.png
|
||||
17
dash/staging/kindle-dash-release/local/low-battery.sh
Normal file
17
dash/staging/kindle-dash-release/local/low-battery.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env sh
|
||||
battery_level_percentage=$1
|
||||
last_battery_report_state="$(dirname "$0")/state/last_battery_report"
|
||||
|
||||
previous_report_timestamp=$(cat "$last_battery_report_state" 2>/dev/null || echo '-1')
|
||||
now=$(date +%s)
|
||||
|
||||
# Implement desired logic here. The example below for example only reports low
|
||||
# battery every 24 hours.
|
||||
|
||||
if [ "$previous_report_timestamp" -eq -1 ] ||
|
||||
[ $((now - previous_report_timestamp)) -gt 86400 ]; then
|
||||
# Replace this with for example an HTTP call via curl, or xh
|
||||
echo "Reporting low battery: $battery_level_percentage%"
|
||||
|
||||
echo "$now" >"$last_battery_report_state"
|
||||
fi
|
||||
BIN
dash/staging/kindle-dash-release/next-wakeup
Executable file
BIN
dash/staging/kindle-dash-release/next-wakeup
Executable file
Binary file not shown.
BIN
dash/staging/kindle-dash-release/sleeping.png
Normal file
BIN
dash/staging/kindle-dash-release/sleeping.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
18
dash/staging/kindle-dash-release/start.sh
Executable file
18
dash/staging/kindle-dash-release/start.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env sh
|
||||
DEBUG=${DEBUG:-false}
|
||||
[ "$DEBUG" = true ] && set -x
|
||||
|
||||
DIR="$(dirname "$0")"
|
||||
ENV_FILE="$DIR/local/env.sh"
|
||||
LOG_FILE="$DIR/logs/dash.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[ -f "$ENV_FILE" ] && . "$ENV_FILE"
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
"$DIR/dash.sh"
|
||||
else
|
||||
"$DIR/dash.sh" >>"$LOG_FILE" 2>&1 &
|
||||
fi
|
||||
2
dash/staging/kindle-dash-release/stop.sh
Executable file
2
dash/staging/kindle-dash-release/stop.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
pkill -f dash.sh
|
||||
26
dash/staging/kindle-dash-release/wait-for-wifi.sh
Executable file
26
dash/staging/kindle-dash-release/wait-for-wifi.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env sh
|
||||
test_ip=$1
|
||||
|
||||
if [ -z "$test_ip" ]; then
|
||||
echo "No test ip specified"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_wifi() {
|
||||
max_retry=30
|
||||
counter=0
|
||||
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
|
||||
# shellcheck disable=SC2181
|
||||
while [ $? -ne 0 ]; do
|
||||
[ $counter -eq $max_retry ] && echo "Couldn't connect to Wi-Fi" && exit 1
|
||||
counter=$((counter + 1))
|
||||
|
||||
sleep 1
|
||||
ping -c 1 "$test_ip" >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_wifi
|
||||
echo "Wi-Fi connected"
|
||||
BIN
dash/staging/kindle-dash-release/xh
Executable file
BIN
dash/staging/kindle-dash-release/xh
Executable file
Binary file not shown.
1
dash/staging/languagebreak/DEVICES.txt
Normal file
1
dash/staging/languagebreak/DEVICES.txt
Normal file
@@ -0,0 +1 @@
|
||||
All the things!
|
||||
135
dash/staging/languagebreak/LanguageBreak/jb
Executable file
135
dash/staging/languagebreak/LanguageBreak/jb
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Quick'n dirty JB key install script for LanguageBarrier.
|
||||
# Based on the "emergency" script from the Hotfix/Bridge restoration package.
|
||||
#
|
||||
# $Id: jb.sh 18327 2021-03-24 18:08:54Z NiLuJe $
|
||||
#
|
||||
##
|
||||
|
||||
# Helper functions, in case the bridge was still kicking.
|
||||
touch /mnt/us/LanguageBreakRan
|
||||
make_mutable() {
|
||||
local my_path="${1}"
|
||||
# NOTE: Can't do that on symlinks, hence the hoop-jumping...
|
||||
if [ -d "${my_path}" ] ; then
|
||||
find "${my_path}" -type d -exec chattr -i '{}' \;
|
||||
find "${my_path}" -type f -exec chattr -i '{}' \;
|
||||
elif [ -f "${my_path}" ] ; then
|
||||
chattr -i "${my_path}"
|
||||
fi
|
||||
}
|
||||
|
||||
# We actually do need that one
|
||||
make_immutable() {
|
||||
local my_path="${1}"
|
||||
if [ -d "${my_path}" ] ; then
|
||||
find "${my_path}" -type d -exec chattr +i '{}' \;
|
||||
find "${my_path}" -type f -exec chattr +i '{}' \;
|
||||
elif [ -f "${my_path}" ] ; then
|
||||
chattr +i "${my_path}"
|
||||
fi
|
||||
}
|
||||
|
||||
POS=1
|
||||
LANGBREAK_LOG="/mnt/us/languagebreak_log"
|
||||
UKSSQSH="/etc/uks.sqsh"
|
||||
jb_log() {
|
||||
f_log "I" "languagebreak" "${2}" "" "${1}"
|
||||
echo "${1}" >> "${LANGBREAK_LOG}"
|
||||
eips 1 "${POS}" "${1}"
|
||||
POS=$((POS+1))
|
||||
sleep 0.2
|
||||
}
|
||||
|
||||
# For logging
|
||||
[ -f "/etc/upstart/functions" ] && source "/etc/upstart/functions"
|
||||
rm -f "${LANGBREAK_LOG}"
|
||||
touch "${LANGBREAK_LOG}"
|
||||
jb_log "LanguageBreak by Marek" "info"
|
||||
jb_log "It was the chinese all along." "info"
|
||||
POS=$((POS+1))
|
||||
jb_log "big thanks to bluebotlabs, GeorgeYellow and Niluje" "info"
|
||||
jb_log "Loaded logging functions" "main"
|
||||
jb_log "I am $(whoami) - $(id)"
|
||||
# Duh'
|
||||
mntroot rw
|
||||
|
||||
# JB first
|
||||
if [ -f $UKSSQSH ] ; then
|
||||
jb_log "${UKSSQSH} - exists - replacing whole sqshfs"
|
||||
make_mutable "${UKSSQSH}"
|
||||
LOOP=$(mount | grep ' on /etc/uks ' | awk '{print $1}')
|
||||
jb_log "Got uks loop device at $LOOP"
|
||||
umount $LOOP
|
||||
losetup -d $LOOP
|
||||
cp /mnt/us/patchedUks.sqsh ${UKSSQSH}
|
||||
mount -o loop=$LOOP,norelatime,nodiratime,noatime -t squashfs ${UKSSQSH} /etc/uks
|
||||
RET=$?
|
||||
if [ $RET -eq 0 ] ; then
|
||||
jb_log "Added developer key :)" "jb"
|
||||
else
|
||||
jb_log "Unable to add developer key (${RET})" "jb"
|
||||
fi
|
||||
POS=$((POS+1))
|
||||
jb_log "$(ls /etc/uks)"
|
||||
|
||||
chown root:root "${UKSSQSH}"
|
||||
chmod 0644 "${UKSSQSH}"
|
||||
make_immutable "${UKSSQSH}"
|
||||
jb_log "Updated permissions for new squashfs keystore" "jb"
|
||||
else
|
||||
jb_log "${UKSSQSH} - doesn't exist - using legacy method"
|
||||
|
||||
if [ -f "/etc/uks/pubdevkey01.pem" ] ; then
|
||||
make_mutable "/etc/uks/pubdevkey01.pem"
|
||||
rm -f "/etc/uks/pubdevkey01.pem"
|
||||
wt_log "Removed existing developer key" "jb"
|
||||
else
|
||||
wt_log "Didn't find existing developer key" "jb"
|
||||
fi
|
||||
cat > "/etc/uks/pubdevkey01.pem" << EOF
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJn1jWU+xxVv/eRKfCPR9e47lP
|
||||
WN2rH33z9QbfnqmCxBRLP6mMjGy6APyycQXg3nPi5fcb75alZo+Oh012HpMe9Lnp
|
||||
eEgloIdm1E4LOsyrz4kttQtGRlzCErmBGt6+cAVEV86y2phOJ3mLk0Ek9UQXbIUf
|
||||
rvyJnS2MKLG2cczjlQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
EOF
|
||||
RET="$?"
|
||||
|
||||
if [ -f "/etc/uks/pubdevkey01.pem" ] ; then
|
||||
wt_log "Created developer key (${RET})" "jb"
|
||||
else
|
||||
wt_log "Unable to create developer key (${RET})" "jb"
|
||||
fi
|
||||
|
||||
chown root:root "/etc/uks/pubdevkey01.pem"
|
||||
chmod 0644 "/etc/uks/pubdevkey01.pem"
|
||||
make_immutable "/etc/uks/pubdevkey01.pem"
|
||||
|
||||
wt_log "Updated permissions for developer key" "jb"
|
||||
fi
|
||||
|
||||
|
||||
# Make sure we can use UYK for OTA packages on FW >= 5.12.x
|
||||
make_mutable "/PRE_GM_DEBUGGING_FEATURES_ENABLED__REMOVE_AT_GMC"
|
||||
rm -rf "/PRE_GM_DEBUGGING_FEATURES_ENABLED__REMOVE_AT_GMC"
|
||||
touch "/PRE_GM_DEBUGGING_FEATURES_ENABLED__REMOVE_AT_GMC"
|
||||
make_immutable "/PRE_GM_DEBUGGING_FEATURES_ENABLED__REMOVE_AT_GMC"
|
||||
jb_log "Enabled developer flag" "br"
|
||||
|
||||
make_mutable "/MNTUS_EXEC"
|
||||
rm -rf "/MNTUS_EXEC"
|
||||
touch "/MNTUS_EXEC"
|
||||
make_immutable "/MNTUS_EXEC"
|
||||
jb_log "Enabled mntus exec flag" "br"
|
||||
|
||||
# Bye
|
||||
sync
|
||||
mntroot ro
|
||||
|
||||
# Finally, change language back to english
|
||||
lipc-send-event com.lab126.blanket.langpicker changeLocale -s "en-US"
|
||||
|
||||
jb_log "Finished installing jailbreak!" "main"
|
||||
BIN
dash/staging/languagebreak/LanguageBreak/patchedUks.sqsh
Normal file
BIN
dash/staging/languagebreak/LanguageBreak/patchedUks.sqsh
Normal file
Binary file not shown.
123
dash/staging/languagebreak/README.MD
Normal file
123
dash/staging/languagebreak/README.MD
Normal file
@@ -0,0 +1,123 @@
|
||||
# **LanguageBreak**
|
||||
Jailbreak for any kindle running FW 5.16.2.1.1 or **LOWER**
|
||||
|
||||
[](https://ko-fi.com/E1E1QLG4D)
|
||||
|
||||
**The exploit works best around version 5.16.2, so if you are on lower firmware you should consider updating**
|
||||
|
||||
Do not update past 5.16.2.1.1 even after jailbreak, there have been big changes since and **everything** is broken, only thing you can do on these versions is downgrade (if your jailbreak survived).
|
||||
|
||||
Big thanks to Bluebotlabs for all the help along the way and GeorgeYellow and bulltricks for bringing the vulnerability to light
|
||||
|
||||
The latest tarball can always be found [here]("https://github.com/notmarek/LanguageBreak/releases/latest")
|
||||
|
||||
##
|
||||
Make sure to remove any kind of password lock - if you forget to this and are stuck on the password screen enter 111222777 and the kindle will factory reset.
|
||||
|
||||
|
||||
Your files **will** be deleted make sure to make a backup.
|
||||
|
||||
# Installation
|
||||
|
||||
## Before jailbreak
|
||||
|
||||
1. Make sure to read the entirety of the instructions **before** proceeding.
|
||||
2. Enable airplane mode
|
||||
3. Make sure that there are no stray .bin files or update.bin.tmp.partial files on the kindle
|
||||
4. Repeat number 3 troughout the proccess
|
||||
|
||||
## Jailbreak
|
||||
|
||||
1. Type ;enter_demo in the Kindle search bar
|
||||
2. Reboot the device
|
||||
3. Once in demo mode, skip setting up wifi and enter random values for store registration
|
||||
4. Skip searching for a demo payload
|
||||
5. Select the "standard" demo type
|
||||
6. Press "Done" at the prompt to sideload content.
|
||||
7. Once the demo is setup, do the "secret gesture" (double finger tap on bottom right of screen then swipe left)
|
||||
8. Enter the demo configuration menu by typing ;demo into the search bar
|
||||
9. Select the "Sideload Content" option
|
||||
10. Copy the contents of the LanguageBreak folder to the Kindle - merging and replacing all files
|
||||
11. Unplug your kindle and go back to the demo menu (viz. step 8)
|
||||
12. Select the "Resell Device" option press Yes/Resell
|
||||
13. Now wait for the press power button to start
|
||||
14. The second it appears plug your kindle back into your computer and copy the contents of the LanguageBreak folder into it once again, overwrite files then safely eject
|
||||
15. Hold the power button as instructed on screen
|
||||
16. A language selection menu should appear in a few seconds
|
||||
17. Choose Chinese (The one above the odd Pseudot language, and/or below Japanese)
|
||||
18. Your kindle should reboot and you should see some log message on the screen
|
||||
|
||||
## After jailbreak
|
||||
|
||||
1. After the device has rebooted, type ;uzb into the search bar
|
||||
2. Connect the device to a PC and copy `Update_hotfix_languagebreak-{language you want to end up with}.bin` to the root of the Kindle storage
|
||||
3. Eject the device and either enter ;dsts or swipe down and select the settings icon to enter the device settings menu
|
||||
4. Select `Update Your Kindle` to install the hotfix
|
||||
5. This will take your device out of demo mode and clean up unneeded jailbreak files.
|
||||
6. You will now probably be in `managed mode`
|
||||
|
||||
## Exiting managed/demo mode after jailbreak
|
||||
|
||||
### Unregistered kindle
|
||||
1. Enter `;demo` into the search bar
|
||||
2. Press the right button
|
||||
3. The device will say that its "entering demo", but will actually reset into normal mode in English
|
||||
4. After this check if you have an mkk folder on your kindle - if it's missing reinstall then hotfix and have fun :)
|
||||
|
||||
### Registered kindle
|
||||
1. Enter `;enter_demo` into the search bar
|
||||
2. Reboot your device
|
||||
3. The device will be in full demo mode so do the setup without wifi and with random values
|
||||
4. Do the secret gesture to get into the kindle UI
|
||||
5. Enter `;demo` into the search bar
|
||||
6. Choose `Resell device` and press `Resell/Yes`
|
||||
7. The device will actually reset into normal mode in English
|
||||
4. After this check if you have an mkk folder on your kindle - if it's missing reinstall then hotfix and have fun :)
|
||||
|
||||
|
||||
# FAQ
|
||||
```
|
||||
Q: How do i check that it worked?
|
||||
A (before installing hotfix): Install hotfix, if you can do that then it worked.
|
||||
A (after installing hotfix): Type `;log` into the search bar, this should show some text at the top of the screen.
|
||||
|
||||
Q: Where are the hotfix files?
|
||||
A: The structure of the tarball is as follows
|
||||
LanguageBreak.tar.gz
|
||||
|-- LanguageBreak
|
||||
| |-- documents
|
||||
| | |-- dictionaries
|
||||
| | | |-- a; export SLASH=$(awk 'BEGIN {print substr(ARGV[1], 0, 1)}' ${PWD}); sh ${SLASH}mnt${SLASH}us${SLASH}jb
|
||||
| | | |-- amisane
|
||||
| |-- DONT_CHECK_BATTERY
|
||||
| |-- jb
|
||||
| |-- patchedUks
|
||||
| |-- .demo
|
||||
| | |-- boot.flag
|
||||
|-- Update_hotfix_languagebreak-*.bin
|
||||
```
|
||||
|
||||
# Troubleshooting
|
||||
Can't seem to get it to work?
|
||||
|
||||
The exploit works best around version 5.16.2, so if you are on lower firmware you should consider updating
|
||||
|
||||
Download the update file of the kindle version you are currently on from amazon install it and try again.
|
||||
|
||||
```
|
||||
PW5: https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_paperwhite_11th_5.XX.X.bin
|
||||
PW4: https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_paperwhite_v2_5.XX.X.bin
|
||||
PW3: https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_paperwhite_5.XX.X.bin
|
||||
Kindle 11th Gen: https://s3.amazonaws.com/firmwaredownloads/update_kindle_11th_5.XX.X.bin
|
||||
Kindle 10th Gen: https://s3.amazonaws.com/firmwaredownloads/update_kindle_10th_5.XX.X.bin
|
||||
Kindle 8th Gen: https://s3.amazonaws.com/firmwaredownloads/update_kindle_8th_5.XX.X.bin
|
||||
Scribe: https://s3.amazonaws.com/firmwaredownloads/update_kindle_scribe_5.XX.X.bin
|
||||
Oasis 10th Gen: https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_oasis_v2_5.XX.X.bin
|
||||
Oasis 9th Gen: https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_oasis_5.XX.X.bin
|
||||
Oasis 8th Gen: https://s3.amazonaws.com/firmwaredownloads/update_kindle_oasis_5.XX.X.bin
|
||||
```
|
||||
|
||||
So version 5.16.2.1.1 for PW4 would be [https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_paperwhite_v2_5.16.2.1.1.bin](]https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_paperwhite_v2_5.16.2.1.1.bin)
|
||||
|
||||
|
||||
[Consider buying me a coffee :)]("https://ko-fi.com/notmarek")
|
||||
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-de-DE.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-de-DE.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-en-GB.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-en-GB.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-en-US.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-en-US.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-AR.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-AR.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-CL.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-CL.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-CO.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-CO.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-ES.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-ES.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-MX.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-es-MX.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-fr-CA.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-fr-CA.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-fr-FR.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-fr-FR.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-it-IT.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-it-IT.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-ja-JP.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-ja-JP.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-nl-NL.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-nl-NL.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-pt-BR.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-pt-BR.bin
Normal file
Binary file not shown.
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-ru-RU.bin
Normal file
BIN
dash/staging/languagebreak/Update_hotfix_languagebreak-ru-RU.bin
Normal file
Binary file not shown.
Binary file not shown.
3259
dash/staging/mrpi/ChangeLog.txt
Normal file
3259
dash/staging/mrpi/ChangeLog.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
dash/staging/mrpi/VERSION
Normal file
1
dash/staging/mrpi/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.7.N @ r19303 on 2023-Nov-06 @ 18:18 - Patched for FW >= 5.16.3
|
||||
27
dash/staging/mrpi/extensions/MRInstaller/CREDITS
Normal file
27
dash/staging/mrpi/extensions/MRInstaller/CREDITS
Normal file
@@ -0,0 +1,27 @@
|
||||
kindletool: KindleTool, Copyright (C) 2011-2015 Yifan Lu, licensed under the GNU General Public License version 3+ (http://www.gnu.org/licenses/gpl.html).
|
||||
(https://github.com/NiLuJe/KindleTool/)
|
||||
|
||||
|
|
||||
|-> libarchive, Copyright (C) Tim Kientzle, licensed under the New BSD License (http://www.opensource.org/licenses/bsd-license.php)
|
||||
| (http://libarchive.github.com/)
|
||||
|
|
||||
|-> GMP, GNU MP Library, Copyright 1991-2013 Free Software Foundation, Inc.,
|
||||
| licensed under the GNU Lesser General Public License version 3+ (http://www.gnu.org/licenses/lgpl.html).
|
||||
| (http://gmplib.org/)
|
||||
|
|
||||
`-> nettle, Copyright (C) 2001-2013 Niels Möller,
|
||||
licensed under the GNU Lesser General Public License version 2.1+ (https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html).
|
||||
(http://www.lysator.liu.se/~nisse/nettle)
|
||||
|
||||
libz: zlib, Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler,
|
||||
Licensed under the zlib license (http://zlib.net/zlib_license.html)
|
||||
(http://zlib.net/)
|
||||
|
||||
fbink: FBInk (FrameBuffer eInker), Copyright (C) 2018-2019 NiLuJe <ninuje@gmail.com>,
|
||||
Released under the GNU General Public License version 3+ (https://www.gnu.org/licenses/gpl.html)
|
||||
(https://github.com/NiLuJe/FBInk)
|
||||
|
||||
BigBlue_Terminal.ttf: BigBlue Terminal +, Copyright (C) VileR, <viler@int10h.org>,
|
||||
Licensed under a Creative Commons Attribution-ShareAlike 4.0 International License (http://creativecommons.org/licenses/by-sa/4.0/)
|
||||
(https://int10h.org/blog/2015/12/bigblue-terminal-oldschool-fixed-width-font/).
|
||||
Patched with extra glyphs via https://github.com/ryanoasis/nerd-fonts/, see the WiKi for individual licenses.
|
||||
1183
dash/staging/mrpi/extensions/MRInstaller/bin/mrinstaller.sh
Executable file
1183
dash/staging/mrpi/extensions/MRInstaller/bin/mrinstaller.sh
Executable file
File diff suppressed because it is too large
Load Diff
12
dash/staging/mrpi/extensions/MRInstaller/config.xml
Executable file
12
dash/staging/mrpi/extensions/MRInstaller/config.xml
Executable file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension>
|
||||
<information>
|
||||
<name>MR Installer</name>
|
||||
<version>1.6</version>
|
||||
<author>NiLuJe</author>
|
||||
<id>MRInstaller</id>
|
||||
</information>
|
||||
<menus>
|
||||
<menu type="json" dynamic="true">menu.json</menu>
|
||||
</menus>
|
||||
</extension>
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user