Initial Commit

This commit is contained in:
RiseForever
2026-08-17 09:17:10 +08:00
parent 7b64db8833
commit 982e1add78
43 changed files with 12801 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/.nyc_output
/coverage
/node_modules
/tests/fixtures
/sign
/dist
/build
+7
View File
@@ -0,0 +1,7 @@
/.nyc_output
/coverage
/node_modules
/sign
/dist
/build
.husky/
+16
View File
@@ -0,0 +1,16 @@
node_modules
build
dist
.DS_Store
Thumbs.db
*.log
*.iml
.idea/
.vscode/
.nyc_output
/coverage
/logs
+22
View File
@@ -0,0 +1,22 @@
module.exports = {
printWidth: 100, // 指定代码长度,超出换行
tabWidth: 2, // tab 键的宽度
useTabs: false, // 使用空格替代tab
semi: false, // 结尾加上分号
singleQuote: false, // 使用单引号
quoteProps: "consistent", // 要求对象字面量属性是否使用引号包裹,(‘as-needed’: 没有特殊要求,禁止使用,'consistent': 保持一致 , preserve: 不限制,想用就用)
trailingComma: "none", // 不添加对象和数组最后一个元素的逗号
bracketSpacing: false, // 对象中对空格和空行进行处理
jsxBracketSameLine: false, // 在多行JSX元素的最后一行追加 >
requirePragma: false, // 是否严格按照文件顶部的特殊注释格式化代码
insertPragma: false, // 是否在格式化的文件顶部插入Pragma标记,以表明该文件被prettier格式化过了
proseWrap: "preserve", // 按照文件原样折行
htmlWhitespaceSensitivity: "ignore", // html文件的空格敏感度,控制空格是否影响布局
endOfLine: "auto", // 结尾是 \n \r \n\r auto
overrides: [
{
files: "*.ux",
options: {parser: "vue"}
}
]
}
+44
View File
@@ -0,0 +1,44 @@
module.exports = {
extends: [
"stylelint-config-standard",
"stylelint-config-recess-order"
// "stylelint-selector-bem-pattern"
],
ignoreFiles: ["node_modules", "test", "dist", "**/*.js"],
rules: {
"no-descending-specificity": null,
"color-hex-case": "lower",
"color-hex-length": "short",
"at-rule-no-unknown": null,
"block-no-empty": null,
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["blur"]
}
],
"property-no-unknown": [
true,
{
ignoreProperties: [
"placeholder-color",
"gradient-start",
"gradient-center",
"gradient-end",
"caret-color",
"selected-color",
"block-color"
]
}
],
"max-line-length": null,
// "indentation": 2,
// "no-empty-source": null,
"selector-type-no-unknown": [
true,
{
ignoreTypes: ["selected-color", "block-color"]
}
]
}
}
+21
View File
@@ -0,0 +1,21 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"bug", // 此项特别针对bug号,用于向测试反馈bug列表的bug修改情况
"feat", // 新功能(feature
"fix", // 修补bug
"docs", // 文档(documentation
"style", // 格式(不影响代码运行的变动)
"refactor", // 重构(即不是新增功能,也不是修改bug的代码变动)
"test", // 增加测试
"chore", // 构建过程或辅助工具的变动
"revert", // feat(pencil): add graphiteWidth option (撤销之前的commit)
"merge" // 合并分支, 例如: merge(前端页面): feature-xxxx修改线程地址
]
]
}
}
+3
View File
@@ -0,0 +1,3 @@
npx husky install
npx husky add .husky/pre-commit 'npx lint-staged'
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
View File
+10306
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "tmt-clock",
"version": "1.0.0",
"description": "",
"engines": {
"node": ">=8.10"
},
"scripts": {
"start": "aiot start --watch",
"build": "aiot build",
"release": "aiot release",
"lint": "eslint --format codeframe --fix --ext .ux,.js src/"
},
"lint-staged": {
"*.{ux,js}": [
"prettier --write",
"eslint --format codeframe --fix",
"git add"
],
"*.{less,css}": [
"prettier --write",
"stylelint --fix --custom-syntax postcss-less",
"git add"
]
},
"devDependencies": {
"@aiot-toolkit/jsc": "^1.0.3",
"aiot-toolkit": "^2.0.5"
}
}
+17
View File
@@ -0,0 +1,17 @@
<script>
import audio from "@system.audio"
export default {
onCreate() {
audio.src = "/common/blank.wav"
audio.loop = true
audio.volume = 0
audio.play()
console.log("app created")
},
onDestroy() {
audio.stop()
console.log("app destroyed")
}
}
</script>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{"a":{"b":"default hello"}}
+1
View File
@@ -0,0 +1 @@
{"a":{"b":"hello"}}
+1
View File
@@ -0,0 +1 @@
{"a":{"b":"你好"}}
+73
View File
@@ -0,0 +1,73 @@
{
"package": "cn.seedsoft.tmtclock",
"name": "番茄钟",
"versionName": "1.0.0",
"versionCode": 1,
"minPlatformVersion": 1000,
"icon": "/common/logo.png",
"deviceTypeList": [
"watch"
],
"features": [
{
"name": "system.router"
},
{
"name": "system.device"
},
{
"name": "system.file"
},
{
"name": "system.vibrator"
},
{
"name": "system.audio"
}
],
"config": {
"logLevel": "log",
"designWidth": "device-width",
"background": {
"features": ["system.audio"]
}
},
"router": {
"entry": "pages/index",
"pages": {
"pages/index": {
"component": "index"
},
"pages/detail": {
"component": "detail"
},
"pages/s4": {
"component": "s4"
},
"pages/s5/home": {
"component": "home"
},
"pages/s5/picker": {
"component": "picker"
},
"pages/s5/timer": {
"component": "timer"
},
"pages/5/home": {
"component": "home"
},
"pages/5/picker": {
"component": "picker"
},
"pages/5/timer": {
"component": "timer"
},
"pages/s4-picker": {
"component": "s4-picker"
},
"pages/s4-timer": {
"component": "s4-timer"
}
}
}
}
+206
View File
@@ -0,0 +1,206 @@
<template>
<div class="home-page">
<text class="page-title">欢迎使用</text>
<text class="clock-label">{{ time }}</text>
<image class="logo" src="/common/logo.png"></image>
<text class="main-title">番茄钟</text>
<div class="settings-entry" onclick="openPicker">
<text class="settings-label">设置时长</text>
<image class="chevron" src="/common/icons/MdiChevronRight.png"></image>
</div>
<div class="start-button" onclick="startTimer">
<image class="button-background" src="/common/icons/button.png"></image>
<image class="play-icon" src="/common/icons/MdiPlay.png"></image>
</div>
</div>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
export default {
private: {
time: "00:00",
timer: null,
duration: "m:20"
},
onInit() {
this.loadDuration()
this.updateTime()
this.timer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
openPicker() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => this.openPickerWithDuration(data.text || "m:20"),
fail: () => this.openPickerWithDuration(this.duration || "m:20")
})
},
openPickerWithDuration(duration) {
router.push({
uri: "/pages/5/picker",
params: {
duration: duration
}
})
},
startTimer() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => this.openTimer(data.text || "m:20"),
fail: () => this.openTimer(this.duration || "m:20")
})
},
openTimer(duration) {
router.push({
uri: "/pages/5/timer",
params: {
duration: duration
}
})
},
loadDuration() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => {
if (data.text) {
this.duration = data.text
}
},
fail: () => {
this.duration = "m:20"
}
})
}
}
</script>
<style>
.home-page {
position: relative;
width: 432px;
height: 514px;
background-color: #000000;
}
.page-title {
position: absolute;
left: 33px;
top: 19px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: left;
}
.clock-label {
position: absolute;
left: 276px;
top: 19px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 30px;
font-weight: 500;
line-height: 33.6px;
text-align: right;
}
.logo {
position: absolute;
left: 152px;
top: 145px;
width: 128px;
height: 128px;
}
.main-title {
position: absolute;
left: 106px;
top: 279px;
width: 220px;
height: 54px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
text-align: center;
}
.settings-entry {
position: absolute;
left: 152px;
top: 333px;
width: 135px;
height: 36px;
opacity: 0.6;
}
.settings-label {
width: 114px;
height: 36px;
color: #ffffff;
font-size: 28px;
font-weight: 500;
line-height: 37.13px;
text-align: center;
}
.chevron {
position: absolute;
left: 114px;
top: 7px;
width: 21px;
height: 21px;
}
.start-button {
position: absolute;
left: 141px;
top: 431px;
width: 151px;
height: 69px;
}
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 151px;
height: 69px;
}
.play-icon {
position: absolute;
left: 53px;
top: 13px;
width: 38px;
height: 38px;
}
</style>
+178
View File
@@ -0,0 +1,178 @@
<template>
<div class="picker-page">
<text class="page-title">设置时长</text>
<text class="clock-label">{{ time }}</text>
<div class="picker-stage">
<picker
class="duration-picker"
type="text"
range="{{durations}}"
selected="{{selectedIndex}}"
onchange="onDurationChange"
></picker>
</div>
<div class="confirm-button" onclick="confirmPicker">
<image class="button-background" src="/common/icons/button.png"></image>
<image class="check-icon" src="/common/icons/check.png"></image>
</div>
</div>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
export default {
protected: {
duration: "m:20"
},
private: {
time: "00:00",
timer: null,
durations: [
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时"
],
selectedIndex: 20,
selectedDurationConfig: "m:20"
},
onInit() {
this.selectedIndex = this.indexForDuration(this.duration)
this.selectedDurationConfig = this.durationConfig(this.durations[this.selectedIndex])
this.updateTime()
this.timer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
onDurationChange(event) {
this.selectedIndex = event.newSelected
const label = this.durations[event.newSelected] || event.newValue
this.selectedDurationConfig = this.durationConfig(label)
},
confirmPicker() {
file.writeText({
uri: "internal://files/focus-duration.txt",
text: this.selectedDurationConfig,
success: () => router.back(),
fail: () => router.back()
})
},
durationValue(label) {
if (label === "5秒") return 5
if (label === "1小时") return 60
if (label === "2小时") return 120
return parseInt(label, 10)
},
durationConfig(label) {
return label === "5秒" ? "s:5" : `m:${this.durationValue(label)}`
},
indexForDuration(config) {
const labels = ["s:5", "m:5", "m:20", "m:40", "m:60", "m:120"]
const normalized = config && config.indexOf(":") < 0 ? `m:${config}` : config
const offset = labels.indexOf(normalized)
return 12 + (offset >= 0 ? offset : 2)
}
}
</script>
<style>
.picker-page {
position: relative;
width: 432px;
height: 514px;
background-color: #000000;
}
.page-title {
position: absolute;
left: 33px;
top: 19px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: left;
}
.clock-label {
position: absolute;
left: 276px;
top: 19px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 30px;
font-weight: 500;
line-height: 33.6px;
text-align: right;
}
.duration-picker {
position: absolute;
left: 0px;
top: 0px;
width: 228px;
height: 162px;
color: rgba(255, 255, 255, 0.6);
font-size: 40px;
selected-color: #ffffff;
selected-font-size: 40px;
}
.picker-stage {
position: absolute;
left: 102px;
top: 176px;
width: 228px;
height: 162px;
}
.confirm-button {
position: absolute;
left: 141px;
top: 431px;
width: 151px;
height: 69px;
}
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 151px;
height: 69px;
}
.check-icon {
position: absolute;
left: 56px;
top: 16px;
width: 38px;
height: 38px;
}
</style>
+304
View File
@@ -0,0 +1,304 @@
<template>
<swiper class="timer-swiper" vertical="true" indicator="false" loop="false">
<div class="timer-page">
<text class="page-title">{{isResting ? "休息中" : "专注中"}}</text>
<text class="clock-label">{{ time }}</text>
<text class="countdown">{{ countdown }}</text>
<text class="focus-label">{{isResting ? "休息中" : "专注中"}}</text>
<div class="stop-button" onclick="stopTimer">
<image class="button-background" src="/common/icons/button_grey.png"></image>
<image class="stop-icon" src="/common/icons/stop.png"></image>
</div>
<div class="pause-button" onclick="togglePause">
<image class="button-background" src="{{isResting ? '/common/icons/button_yellow.png' : '/common/icons/button2.png'}}"></image>
<image class="pause-icon" src="{{paused ? '/common/icons/MdiPlay.png' : '/common/icons/pause.png'}}"></image>
</div>
</div>
<div class="stats-page">
<text class="page-title">专注统计</text>
<text class="clock-label">{{ time }}</text>
<text class="stats-title focus-title">累计专注</text>
<text class="stats-value focus-value">{{ focusTotalText }}</text>
<text class="stats-title rest-title">累计休息</text>
<text class="stats-value rest-value">{{ restTotalText }}</text>
</div>
</swiper>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
import vibrator from "@system.vibrator"
export default {
protected: { duration: "m:20" },
private: {
time: "00:00",
countdown: "20:00",
clockTimer: null,
countdownTimer: null,
remaining: 1200,
paused: false,
isResting: false,
focusDurationSeconds: 1200,
focusTotalSeconds: 0,
restTotalSeconds: 0,
focusTotalText: "00:00:00",
restTotalText: "00:00:00"
},
onInit() {
this.loadStats()
this.startCountdown(this.durationSeconds(this.duration))
this.updateTime()
this.clockTimer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
clearInterval(this.clockTimer)
clearInterval(this.countdownTimer)
},
startCountdown(seconds) {
clearInterval(this.countdownTimer)
this.focusDurationSeconds = seconds > 0 ? seconds : 1200
this.remaining = this.focusDurationSeconds
this.isResting = false
this.paused = false
this.updateCountdown()
this.countdownTimer = setInterval(() => this.tick(), 1000)
},
startRest() {
this.remaining = 300
this.isResting = true
this.paused = true
this.updateCountdown()
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
tick() {
if (this.paused || this.remaining <= 0) return
this.remaining -= 1
if (this.isResting) this.restTotalSeconds += 1
else this.focusTotalSeconds += 1
this.updateStats()
this.persistStats()
this.updateCountdown()
if (this.remaining === 0) {
if (this.isResting) {
vibrator.vibrate({ mode: "short" })
this.startCountdown(this.focusDurationSeconds)
} else {
vibrator.vibrate({ mode: "long" })
this.startRest()
}
}
},
durationSeconds(config) {
if (config && config.indexOf("s:") === 0) return parseInt(config.substring(2), 10) || 5
if (config && config.indexOf("m:") === 0) return (parseInt(config.substring(2), 10) || 20) * 60
return (parseInt(config, 10) || 20) * 60
},
updateCountdown() {
const minutes = Math.floor(this.remaining / 60)
const seconds = this.remaining % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.countdown = `${pad(minutes)}:${pad(seconds)}`
},
updateStats() {
this.focusTotalText = this.formatDuration(this.focusTotalSeconds)
this.restTotalText = this.formatDuration(this.restTotalSeconds)
},
formatDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
},
loadStats() {
file.readText({
uri: "internal://files/focus-stats.json",
success: (data) => {
try {
const stats = JSON.parse(data.text)
if (stats.date === this.today()) {
this.focusTotalSeconds = parseInt(stats.focus, 10) || 0
this.restTotalSeconds = parseInt(stats.rest, 10) || 0
}
} catch (error) {}
this.updateStats()
},
fail: () => this.updateStats()
})
},
persistStats() {
file.writeText({
uri: "internal://files/focus-stats.json",
text: JSON.stringify({
date: this.today(),
focus: this.focusTotalSeconds,
rest: this.restTotalSeconds
})
})
},
today() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
},
togglePause() {
this.paused = !this.paused
},
stopTimer() {
router.back()
}
}
</script>
<style>
.timer-swiper,
.timer-page,
.stats-page {
width: 432px;
height: 514px;
background-color: #000000;
}
.timer-page,
.stats-page {
position: relative;
}
.page-title {
position: absolute;
left: 33px;
top: 19px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: left;
}
.clock-label {
position: absolute;
left: 276px;
top: 19px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 30px;
font-weight: 500;
line-height: 33.6px;
text-align: right;
}
.countdown {
position: absolute;
left: 66px;
top: 189px;
width: 300px;
height: 97px;
color: #ffffff;
font-size: 64px;
font-weight: 500;
line-height: 84.86px;
text-align: center;
}
.focus-label {
position: absolute;
left: 66px;
top: 287px;
width: 300px;
height: 38px;
color: rgba(255, 255, 255, 0.6);
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
text-align: center;
}
.stop-button,
.pause-button {
position: absolute;
top: 435px;
width: 140px;
height: 69px;
}
.stop-button { left: 66px; }
.pause-button { left: 226px; }
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 140px;
height: 69px;
}
.stop-icon {
position: absolute;
left: 59px;
top: 23px;
width: 22px;
height: 24px;
}
.pause-icon {
position: absolute;
left: 53px;
top: 16px;
width: 35px;
height: 38px;
}
.stats-title,
.stats-value {
position: absolute;
left: 66px;
width: 300px;
text-align: center;
}
.stats-title {
height: 43px;
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
}
.stats-value {
height: 50px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
}
.focus-title { top: 161px; color: rgba(112, 119, 255, 1); }
.focus-value { top: 204px; }
.rest-title { top: 260px; color: rgba(149, 168, 0, 1); }
.rest-value { top: 303px; }
</style>
+26
View File
@@ -0,0 +1,26 @@
<template>
<div class="demo-page">
<text class="title">{{ text }}</text>
</div>
</template>
<script>
export default {
private: {
text: "欢迎打开详情页"
}
}
</script>
<style>
.demo-page {
flex-direction: column;
justify-content: center;
align-items: center;
}
.title {
font-size: 20px;
text-align: center;
}
</style>
+56
View File
@@ -0,0 +1,56 @@
<template>
<div class="demo-page">
<text class="title">正在识别设备屏幕</text>
</div>
</template>
<script>
import router from "@system.router"
import device from "@system.device"
export default {
onInit() {
device.getInfo({
success: (info) => {
if (info.screenWidth === 432 && info.screenHeight === 514) {
router.replace({
uri: "/pages/5/home"
})
} else if (info.screenWidth === 480 && info.screenHeight === 480) {
router.replace({
uri: "/pages/s5/home"
})
} else if (info.screenWidth === 466 && info.screenHeight === 466) {
router.replace({
uri: "/pages/s4"
})
} else {
router.replace({
uri: "/pages/detail"
})
}
},
fail: () => {
router.replace({
uri: "/pages/detail"
})
}
})
}
}
</script>
<style>
.demo-page {
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #000000;
}
.title {
font-size: 20px;
text-align: center;
color: #ffffff;
}
</style>
View File
+191
View File
@@ -0,0 +1,191 @@
<template>
<div class="picker-page">
<text class="clock-label">{{ time }}</text>
<text class="page-title">设置时长</text>
<div class="picker-stage">
<picker
class="duration-picker"
type="text"
range="{{durations}}"
selected="{{selectedIndex}}"
onchange="onDurationChange"
></picker>
</div>
<div class="confirm-button" onclick="confirmPicker">
<image class="button-background" src="/common/icons/button.png"></image>
<image class="check-icon" src="/common/icons/check.png"></image>
</div>
</div>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
export default {
protected: {
duration: "m:20"
},
private: {
time: "00:00",
timer: null,
durations: [
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时"
],
selectedIndex: 20,
selectedDuration: 1200,
selectedDurationConfig: "m:20"
},
onInit() {
this.selectedIndex = this.indexForDuration(this.duration)
this.selectedDuration = this.durationValue(this.durations[this.selectedIndex])
this.selectedDurationConfig = this.durationConfig(this.durations[this.selectedIndex])
this.updateTime()
this.timer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
onDurationChange(event) {
this.selectedIndex = event.newSelected
const label = this.durations[event.newSelected] || event.newValue
this.selectedDuration = this.durationValue(label)
this.selectedDurationConfig = this.durationConfig(label)
},
confirmPicker() {
file.writeText({
uri: "internal://files/focus-duration.txt",
text: this.selectedDurationConfig,
success: () => router.back(),
fail: () => router.back()
})
},
durationValue(label) {
if (label === "5秒") {
return 5
}
if (label === "1小时") {
return 60
}
if (label === "2小时") {
return 120
}
return parseInt(label, 10)
},
durationConfig(label) {
return label === "5秒" ? "s:5" : `m:${this.durationValue(label)}`
},
indexForDuration(config) {
const labels = ["s:5", "m:5", "m:20", "m:40", "m:60", "m:120"]
const legacyConfig = config && config.indexOf(":") < 0
? `m:${config}`
: config
const offset = labels.indexOf(legacyConfig)
return 12 + (offset >= 0 ? offset : 2)
},
}
</script>
<style>
.picker-page {
position: relative;
width: 466px;
height: 466px;
background-color: #000000;
}
.clock-label {
position: absolute;
left: 169px;
top: 0px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 28px;
font-weight: 600;
line-height: 33.6px;
text-align: center;
}
.page-title {
position: absolute;
left: 107px;
top: 34px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: center;
}
.duration-picker {
position: absolute;
top: 0px;
left: 0px;
width: 228px;
height: 162px;
color: rgba(255, 255, 255, 0.6);
font-size: 40px;
selected-color: #ffffff;
selected-font-size: 40px;
}
.picker-stage {
position: absolute;
left: 119px;
top: 152px;
width: 228px;
height: 162px;
}
.confirm-button {
position: absolute;
left: 158px;
top: 391px;
width: 151px;
height: 69px;
}
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 151px;
height: 69px;
}
.check-icon {
position: absolute;
left: 56px;
top: 16px;
width: 38px;
height: 38px;
}
</style>
+400
View File
@@ -0,0 +1,400 @@
<template>
<swiper class="timer-swiper" vertical="true" indicator="false" loop="false">
<div class="timer-page">
<text class="clock-label">{{ time }}</text>
<text class="page-title">{{isResting ? "休息中" : "专注中"}}</text>
<text class="countdown">{{ countdown }}</text>
<text class="focus-label">{{isResting ? "休息中" : "专注中"}}</text>
<div class="stop-button" onclick="stopTimer">
<image class="stop-background" src="/common/icons/button_grey.png"></image>
<image class="stop-icon" src="/common/icons/stop.png"></image>
</div>
<div class="pause-button" onclick="togglePause">
<image
class="pause-background"
src="{{isResting ? '/common/icons/button_yellow.png' : '/common/icons/button2.png'}}"
></image>
<image
class="pause-icon"
src="{{paused ? '/common/icons/MdiPlay.png' : '/common/icons/pause.png'}}"
></image>
</div>
</div>
<div class="stats-page">
<text class="stats-clock-label">{{ time }}</text>
<text class="stats-page-title">专注统计</text>
<text class="focus-total-title">累计专注</text>
<text class="focus-total-value">{{ focusTotalText }}</text>
<text class="rest-total-title">累计休息</text>
<text class="rest-total-value">{{ restTotalText }}</text>
</div>
</swiper>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
import vibrator from "@system.vibrator"
export default {
protected: {
duration: "m:20"
},
private: {
time: "00:00",
countdown: "20:00",
clockTimer: null,
countdownTimer: null,
remaining: 1200,
paused: false,
isResting: false,
focusDurationSeconds: 1200,
focusTotalSeconds: 0,
restTotalSeconds: 0,
focusTotalText: "00:00:00",
restTotalText: "00:00:00"
},
onInit() {
this.loadStats()
const duration = this.durationSeconds(this.duration)
if (duration > 0) {
this.startCountdown(duration)
} else {
this.loadDuration()
}
this.updateTime()
this.clockTimer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
clearInterval(this.clockTimer)
clearInterval(this.countdownTimer)
},
loadDuration() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => this.startCountdown(this.durationSeconds(data.text)),
fail: () => this.startCountdown(1200)
})
},
startCountdown(seconds) {
clearInterval(this.countdownTimer)
this.focusDurationSeconds = seconds > 0 ? seconds : 1200
this.remaining = this.focusDurationSeconds
this.isResting = false
this.paused = false
this.updateCountdown()
this.countdownTimer = setInterval(() => this.tick(), 1000)
},
startRest() {
this.remaining = 5 * 60
this.isResting = true
this.paused = true
this.updateCountdown()
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
tick() {
if (this.paused || this.remaining <= 0) {
return
}
this.remaining -= 1
if (this.isResting) {
this.restTotalSeconds += 1
} else {
this.focusTotalSeconds += 1
}
this.updateStats()
this.persistStats()
this.updateCountdown()
if (this.remaining === 0) {
if (this.isResting) {
vibrator.vibrate({
mode: "short"
})
this.startCountdown(this.focusDurationSeconds)
} else {
vibrator.vibrate({
mode: "long"
})
this.startRest()
}
}
},
updateCountdown() {
const minutes = Math.floor(this.remaining / 60)
const seconds = this.remaining % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.countdown = `${pad(minutes)}:${pad(seconds)}`
},
durationSeconds(config) {
if (config && config.indexOf("s:") === 0) {
return parseInt(config.substring(2), 10) || 5
}
if (config && config.indexOf("m:") === 0) {
return (parseInt(config.substring(2), 10) || 20) * 60
}
return (parseInt(config, 10) || 20) * 60
},
updateStats() {
this.focusTotalText = this.formatDuration(this.focusTotalSeconds)
this.restTotalText = this.formatDuration(this.restTotalSeconds)
},
formatDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
},
loadStats() {
file.readText({
uri: "internal://files/focus-stats.json",
success: (data) => {
try {
const stats = JSON.parse(data.text)
if (stats.date === this.today()) {
this.focusTotalSeconds = parseInt(stats.focus, 10) || 0
this.restTotalSeconds = parseInt(stats.rest, 10) || 0
}
this.updateStats()
} catch (error) {
this.updateStats()
}
},
fail: () => this.updateStats()
})
},
persistStats() {
file.writeText({
uri: "internal://files/focus-stats.json",
text: JSON.stringify({
date: this.today(),
focus: this.focusTotalSeconds,
rest: this.restTotalSeconds
})
})
},
today() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
},
togglePause() {
this.paused = !this.paused
},
stopTimer() {
router.back()
}
}
</script>
<style>
.timer-swiper {
width: 466px;
height: 466px;
background-color: #000000;
}
.timer-page {
position: relative;
width: 466px;
height: 466px;
background-color: #000000;
}
.clock-label {
position: absolute;
left: 169px;
top: 0px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 28px;
font-weight: 600;
line-height: 33.6px;
text-align: center;
}
.page-title {
position: absolute;
left: 107px;
top: 34px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: center;
}
.countdown {
position: absolute;
left: 83px;
top: 165px;
width: 300px;
height: 97px;
color: #ffffff;
font-size: 64px;
font-weight: 500;
line-height: 84.86px;
text-align: center;
}
.focus-label {
position: absolute;
left: 83px;
top: 263px;
width: 300px;
height: 38px;
color: rgba(255, 255, 255, 0.6);
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
text-align: center;
}
.stop-button,
.pause-button {
position: absolute;
top: 358px;
width: 140px;
height: 69px;
}
.stop-button {
left: 83px;
}
.pause-button {
left: 243px;
}
.stop-background,
.pause-background {
position: absolute;
left: 0px;
top: 0px;
width: 140px;
height: 69px;
}
.stop-icon {
position: absolute;
left: 59px;
top: 23px;
width: 22px;
height: 24px;
}
.pause-icon {
position: absolute;
left: 53px;
top: 16px;
width: 35px;
height: 38px;
}
.stats-page {
position: relative;
width: 466px;
height: 466px;
background-color: #000000;
}
.stats-clock-label {
position: absolute;
left: 169px;
top: 0px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 28px;
font-weight: 600;
line-height: 33.6px;
text-align: center;
}
.stats-page-title {
position: absolute;
left: 107px;
top: 34px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: center;
}
.focus-total-title,
.rest-total-title,
.focus-total-value,
.rest-total-value {
position: absolute;
left: 83px;
width: 300px;
text-align: center;
}
.focus-total-title {
top: 134px;
height: 43px;
color: rgba(112, 119, 255, 1);
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
}
.focus-total-value {
top: 177px;
height: 50px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
}
.rest-total-title {
top: 233px;
height: 43px;
color: rgba(149, 168, 0, 1);
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
}
.rest-total-value {
top: 276px;
height: 50px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
}
</style>
View File
+214
View File
@@ -0,0 +1,214 @@
<template>
<div class="home-page">
<text class="clock-label">{{ time }}</text>
<text class="page-title">欢迎使用</text>
<image class="logo" src="/common/logo.png"></image>
<text class="main-title">番茄钟</text>
<div class="settings-entry" onclick="openPicker">
<text class="settings-label">设置时长</text>
<image class="chevron" src="/common/icons/MdiChevronRight.png"></image>
</div>
<div class="start-button" onclick="startTimer">
<image class="button-background" src="/common/icons/button.png"></image>
<image class="play-icon" src="/common/icons/MdiPlay.png"></image>
</div>
</div>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
export default {
private: {
time: "00:00",
timer: null,
duration: 20
},
onInit() {
this.loadDuration()
this.updateTime()
this.timer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
openPicker() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => {
this.openPickerWithDuration(data.text || "m:20")
},
fail: () => {
this.openPickerWithDuration("m:20")
}
})
},
openPickerWithDuration(duration) {
router.push({
uri: "/pages/s4-picker",
params: {
duration: `${duration}`
}
})
},
startTimer() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => {
this.openTimer(data.text || "m:20")
},
fail: () => {
this.openTimer("m:20")
}
})
},
openTimer(duration) {
router.push({
uri: "/pages/s4-timer",
params: {
duration: duration
}
})
},
loadDuration() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => {
if (data.text) {
this.duration = data.text
}
},
fail: () => {
this.duration = "m:20"
}
})
}
}
</script>
<style>
.home-page {
position: relative;
width: 466px;
height: 466px;
background-color: #000000;
}
.clock-label {
position: absolute;
left: 169px;
top: 0px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 28px;
font-weight: 600;
line-height: 33.6px;
text-align: center;
}
.page-title {
position: absolute;
left: 107px;
top: 34px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: center;
}
.logo {
position: absolute;
left: 170px;
top: 121px;
width: 128px;
height: 128px;
}
.main-title {
position: absolute;
left: 124px;
top: 255px;
width: 220px;
height: 54px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
text-align: center;
}
.settings-entry {
position: absolute;
left: 170px;
top: 309px;
width: 135px;
height: 36px;
opacity: 0.6;
}
.settings-label {
width: 114px;
height: 36px;
color: #ffffff;
font-size: 28px;
font-weight: 500;
line-height: 37.13px;
text-align: center;
}
.chevron {
position: absolute;
left: 114px;
top: 7px;
width: 21px;
height: 21px;
}
.start-button {
position: absolute;
left: 158px;
top: 391px;
width: 151px;
height: 69px;
}
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 151px;
height: 69px;
}
.play-icon {
position: absolute;
left: 53px;
top: 13px;
width: 38px;
height: 38px;
}
</style>
View File
+206
View File
@@ -0,0 +1,206 @@
<template>
<div class="home-page">
<text class="clock-label">{{ time }}</text>
<text class="page-title">欢迎使用</text>
<image class="logo" src="/common/logo.png"></image>
<text class="main-title">番茄钟</text>
<div class="settings-entry" onclick="openPicker">
<text class="settings-label">设置时长</text>
<image class="chevron" src="/common/icons/MdiChevronRight.png"></image>
</div>
<div class="start-button" onclick="startTimer">
<image class="button-background" src="/common/icons/button.png"></image>
<image class="play-icon" src="/common/icons/MdiPlay.png"></image>
</div>
</div>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
export default {
private: {
time: "00:00",
timer: null,
duration: "m:20"
},
onInit() {
this.loadDuration()
this.updateTime()
this.timer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
openPicker() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => this.openPickerWithDuration(data.text || "m:20"),
fail: () => this.openPickerWithDuration(this.duration || "m:20")
})
},
openPickerWithDuration(duration) {
router.push({
uri: "/pages/s5/picker",
params: {
duration: duration
}
})
},
startTimer() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => this.openTimer(data.text || "m:20"),
fail: () => this.openTimer(this.duration || "m:20")
})
},
openTimer(duration) {
router.push({
uri: "/pages/s5/timer",
params: {
duration: duration
}
})
},
loadDuration() {
file.readText({
uri: "internal://files/focus-duration.txt",
success: (data) => {
if (data.text) {
this.duration = data.text
}
},
fail: () => {
this.duration = "m:20"
}
})
}
}
</script>
<style>
.home-page {
position: relative;
width: 480px;
height: 480px;
background-color: #000000;
}
.clock-label {
position: absolute;
left: 175px;
top: 0px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 28px;
font-weight: 600;
line-height: 33.6px;
text-align: center;
}
.page-title {
position: absolute;
left: 114px;
top: 41px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: center;
}
.logo {
position: absolute;
left: 176px;
top: 128px;
width: 128px;
height: 128px;
}
.main-title {
position: absolute;
left: 131px;
top: 262px;
width: 220px;
height: 54px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
text-align: center;
}
.settings-entry {
position: absolute;
left: 176px;
top: 316px;
width: 135px;
height: 36px;
opacity: 0.6;
}
.settings-label {
width: 114px;
height: 36px;
color: #ffffff;
font-size: 28px;
font-weight: 500;
line-height: 37.13px;
text-align: center;
}
.chevron {
position: absolute;
left: 114px;
top: 7px;
width: 21px;
height: 21px;
}
.start-button {
position: absolute;
left: 165px;
top: 401px;
width: 151px;
height: 69px;
}
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 151px;
height: 69px;
}
.play-icon {
position: absolute;
left: 53px;
top: 13px;
width: 38px;
height: 38px;
}
</style>
+187
View File
@@ -0,0 +1,187 @@
<template>
<div class="picker-page">
<text class="clock-label">{{ time }}</text>
<text class="page-title">设置时长</text>
<div class="picker-stage">
<picker
class="duration-picker"
type="text"
range="{{durations}}"
selected="{{selectedIndex}}"
onchange="onDurationChange"
></picker>
</div>
<div class="confirm-button" onclick="confirmPicker">
<image class="button-background" src="/common/icons/button.png"></image>
<image class="check-icon" src="/common/icons/check.png"></image>
</div>
</div>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
export default {
protected: {
duration: "m:20"
},
private: {
time: "00:00",
timer: null,
durations: [
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时",
"5秒", "5分钟", "20分钟", "40分钟", "1小时", "2小时"
],
selectedIndex: 20,
selectedDurationConfig: "m:20"
},
onInit() {
this.selectedIndex = this.indexForDuration(this.duration)
this.selectedDurationConfig = this.durationConfig(this.durations[this.selectedIndex])
this.updateTime()
this.timer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
onDurationChange(event) {
this.selectedIndex = event.newSelected
const label = this.durations[event.newSelected] || event.newValue
this.selectedDurationConfig = this.durationConfig(label)
},
confirmPicker() {
file.writeText({
uri: "internal://files/focus-duration.txt",
text: this.selectedDurationConfig,
success: () => router.back(),
fail: () => router.back()
})
},
durationValue(label) {
if (label === "5秒") {
return 5
}
if (label === "1小时") {
return 60
}
if (label === "2小时") {
return 120
}
return parseInt(label, 10)
},
durationConfig(label) {
return label === "5秒" ? "s:5" : `m:${this.durationValue(label)}`
},
indexForDuration(config) {
const labels = ["s:5", "m:5", "m:20", "m:40", "m:60", "m:120"]
const legacyConfig = config && config.indexOf(":") < 0
? `m:${config}`
: config
const offset = labels.indexOf(legacyConfig)
return 12 + (offset >= 0 ? offset : 2)
}
}
</script>
<style>
.picker-page {
position: relative;
width: 480px;
height: 480px;
background-color: #000000;
}
.clock-label {
position: absolute;
left: 175px;
top: 0px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 28px;
font-weight: 600;
line-height: 33.6px;
text-align: center;
}
.page-title {
position: absolute;
left: 114px;
top: 41px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: center;
}
.duration-picker {
position: absolute;
left: 0px;
top: 0px;
width: 228px;
height: 162px;
color: rgba(255, 255, 255, 0.6);
font-size: 40px;
selected-color: #ffffff;
selected-font-size: 40px;
}
.picker-stage {
position: absolute;
left: 126px;
top: 159px;
width: 228px;
height: 162px;
}
.confirm-button {
position: absolute;
left: 165px;
top: 401px;
width: 151px;
height: 69px;
}
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 151px;
height: 69px;
}
.check-icon {
position: absolute;
left: 56px;
top: 16px;
width: 38px;
height: 38px;
}
</style>
+283
View File
@@ -0,0 +1,283 @@
<template>
<swiper class="timer-swiper" vertical="true" indicator="false" loop="false">
<div class="timer-page">
<text class="clock-label">{{ time }}</text>
<text class="page-title">{{isResting ? "休息中" : "专注中"}}</text>
<text class="countdown">{{ countdown }}</text>
<text class="focus-label">{{isResting ? "休息中" : "专注中"}}</text>
<div class="stop-button" onclick="stopTimer">
<image class="button-background" src="/common/icons/button_grey.png"></image>
<image class="stop-icon" src="/common/icons/stop.png"></image>
</div>
<div class="pause-button" onclick="togglePause">
<image class="button-background" src="{{isResting ? '/common/icons/button_yellow.png' : '/common/icons/button2.png'}}"></image>
<image class="pause-icon" src="{{paused ? '/common/icons/MdiPlay.png' : '/common/icons/pause.png'}}"></image>
</div>
</div>
<div class="stats-page">
<text class="clock-label">{{ time }}</text>
<text class="page-title">专注统计</text>
<text class="stats-title focus-title">累计专注</text>
<text class="stats-value focus-value">{{ focusTotalText }}</text>
<text class="stats-title rest-title">累计休息</text>
<text class="stats-value rest-value">{{ restTotalText }}</text>
</div>
</swiper>
</template>
<script>
import router from "@system.router"
import file from "@system.file"
import vibrator from "@system.vibrator"
export default {
protected: { duration: "m:20" },
private: {
time: "00:00", countdown: "20:00", clockTimer: null, countdownTimer: null,
remaining: 1200, paused: false, isResting: false, focusDurationSeconds: 1200,
focusTotalSeconds: 0, restTotalSeconds: 0,
focusTotalText: "00:00:00", restTotalText: "00:00:00"
},
onInit() {
this.loadStats()
const seconds = this.durationSeconds(this.duration)
this.startCountdown(seconds > 0 ? seconds : 1200)
this.updateTime()
this.clockTimer = setInterval(() => this.updateTime(), 1000)
},
onDestroy() {
clearInterval(this.clockTimer)
clearInterval(this.countdownTimer)
},
startCountdown(seconds) {
clearInterval(this.countdownTimer)
this.focusDurationSeconds = seconds > 0 ? seconds : 1200
this.remaining = this.focusDurationSeconds
this.isResting = false
this.paused = false
this.updateCountdown()
this.countdownTimer = setInterval(() => this.tick(), 1000)
},
startRest() {
this.remaining = 300
this.isResting = true
this.paused = true
this.updateCountdown()
},
updateTime() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
},
tick() {
if (this.paused || this.remaining <= 0) return
this.remaining -= 1
if (this.isResting) this.restTotalSeconds += 1
else this.focusTotalSeconds += 1
this.updateStats()
this.persistStats()
this.updateCountdown()
if (this.remaining === 0) {
if (this.isResting) {
vibrator.vibrate({ mode: "short" })
this.startCountdown(this.focusDurationSeconds)
} else {
vibrator.vibrate({ mode: "long" })
this.startRest()
}
}
},
durationSeconds(config) {
if (config && config.indexOf("s:") === 0) return parseInt(config.substring(2), 10) || 5
if (config && config.indexOf("m:") === 0) return (parseInt(config.substring(2), 10) || 20) * 60
return (parseInt(config, 10) || 20) * 60
},
updateCountdown() {
const minutes = Math.floor(this.remaining / 60)
const seconds = this.remaining % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
this.countdown = `${pad(minutes)}:${pad(seconds)}`
},
updateStats() {
this.focusTotalText = this.formatDuration(this.focusTotalSeconds)
this.restTotalText = this.formatDuration(this.restTotalSeconds)
},
formatDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
},
loadStats() {
file.readText({
uri: "internal://files/focus-stats.json",
success: (data) => {
try {
const stats = JSON.parse(data.text)
if (stats.date === this.today()) {
this.focusTotalSeconds = parseInt(stats.focus, 10) || 0
this.restTotalSeconds = parseInt(stats.rest, 10) || 0
}
} catch (error) {}
this.updateStats()
},
fail: () => this.updateStats()
})
},
persistStats() {
file.writeText({
uri: "internal://files/focus-stats.json",
text: JSON.stringify({
date: this.today(),
focus: this.focusTotalSeconds,
rest: this.restTotalSeconds
})
})
},
today() {
const now = new Date()
const pad = (value) => (value < 10 ? `0${value}` : `${value}`)
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
},
togglePause() { this.paused = !this.paused },
stopTimer() { router.back() }
}
</script>
<style>
.timer-swiper, .timer-page, .stats-page {
width: 480px;
height: 480px;
background-color: #000000;
}
.timer-page, .stats-page { position: relative; }
.clock-label {
position: absolute;
left: 175px;
top: 0px;
width: 130px;
height: 45px;
color: rgba(255, 255, 255, 0.6);
font-size: 28px;
font-weight: 600;
line-height: 33.6px;
text-align: center;
}
.page-title {
position: absolute;
left: 114px;
top: 41px;
width: 255px;
height: 45px;
color: #ffffff;
font-size: 30px;
font-weight: 500;
line-height: 44.8px;
text-align: center;
}
.countdown {
position: absolute;
left: 90px;
top: 172px;
width: 300px;
height: 97px;
color: #ffffff;
font-size: 64px;
font-weight: 500;
line-height: 84.86px;
text-align: center;
}
.focus-label {
position: absolute;
left: 90px;
top: 270px;
width: 300px;
height: 38px;
color: rgba(255, 255, 255, 0.6);
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
text-align: center;
}
.stop-button, .pause-button {
position: absolute;
top: 373px;
width: 140px;
height: 69px;
}
.stop-button { left: 91px; }
.pause-button { left: 251px; }
.button-background {
position: absolute;
left: 0px;
top: 0px;
width: 140px;
height: 69px;
}
.stop-icon {
position: absolute;
left: 59px;
top: 23px;
width: 22px;
height: 24px;
}
.pause-icon {
position: absolute;
left: 53px;
top: 16px;
width: 35px;
height: 38px;
}
.stats-title, .stats-value {
position: absolute;
left: 90px;
width: 300px;
text-align: center;
}
.stats-title {
height: 43px;
font-size: 32px;
font-weight: 500;
line-height: 42.43px;
}
.stats-value {
height: 50px;
color: #ffffff;
font-size: 40px;
font-weight: 500;
line-height: 53.04px;
}
.focus-title { top: 141px; color: rgba(112, 119, 255, 1); }
.focus-value { top: 184px; }
.rest-title { top: 240px; color: rgba(149, 168, 0, 1); }
.rest-value { top: 283px; }
</style>