Skip to content

Commit 36a9b3a

Browse files
committed
first commit
0 parents  commit 36a9b3a

File tree

11 files changed

+804
-0
lines changed

11 files changed

+804
-0
lines changed

.github/workflows/build_apk.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Build Android APK
2+
3+
on:
4+
push:
5+
branches: [ "main", "master" ]
6+
workflow_dispatch:
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v3
14+
15+
- uses: actions/setup-java@v3
16+
with:
17+
distribution: 'zulu'
18+
java-version: '17'
19+
20+
- uses: subosito/flutter-action@v2
21+
with:
22+
channel: 'stable'
23+
24+
- name: Get dependencies
25+
run: flutter pub get
26+
27+
- name: Build APK
28+
run: flutter build apk --release --no-tree-shake-icons
29+
30+
- name: Upload APK
31+
uses: actions/upload-artifact@v4
32+
with:
33+
name: punch-card-record-apk
34+
path: build/app/outputs/flutter-apk/app-release.apk

FLUTTER_INSTALLATION.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Flutter Windows 安装与环境配置指南
2+
3+
本指南将帮助您在 Windows 系统上从零开始搭建 Flutter 开发环境,以便运行“打卡记”应用。
4+
5+
## 第一步:下载 Flutter SDK
6+
7+
1. 访问 Flutter 官网下载页:[https://docs.flutter.dev/get-started/install/windows](https://docs.flutter.dev/get-started/install/windows)
8+
2. 点击下载蓝色的 **Stable Channel** 按钮(例如 `flutter_windows_3.x.x-stable.zip`)。
9+
3. **解压文件**
10+
* 将压缩包解压到一个路径简单且**不包含中文或空格**的文件夹中。
11+
* 推荐位置:`C:\src\flutter`
12+
13+
## 第二步:配置环境变量 (Path)
14+
15+
为了在任何终端都能使用 `flutter` 命令,需要将其添加到系统环境变量中。
16+
17+
1. 在 Windows 搜索栏输入“**编辑系统环境变量**”并打开。
18+
2. 点击右下角的“**环境变量**”按钮。
19+
3. 在“**用户变量**”一栏中,找到名为 `Path` 的变量,选中并点击“**编辑**”。
20+
4. 点击“**新建**”,输入您刚才解压的 flutter `bin` 目录的完整路径。
21+
* 例如:`C:\src\flutter\bin`
22+
5. 连续点击“确定”保存所有设置。
23+
24+
**验证**:打开一个新的 PowerShell 或 CMD 窗口,输入 `flutter --version`。如果显示版本号,说明配置成功。
25+
26+
## 第三步:安装 Android Studio (用于安卓开发工具链)
27+
28+
虽然我们使用 VS Code 写代码,但编译安卓应用需要 Android Studio 提供的 SDK 和工具链。
29+
30+
1. 下载并安装 **Android Studio**[https://developer.android.com/studio](https://developer.android.com/studio)
31+
2. 启动 Android Studio,按照向导进行“**Standard**” (标准) 安装。这将自动下载最新的 Android SDK。
32+
3. **关键步骤:安装命令行工具**
33+
* 打开 Android Studio。
34+
* 点击 **More Actions** > **SDK Manager**
35+
* 切换到 **SDK Tools** 选项卡。
36+
* 勾选 **Android SDK Command-line Tools (latest)**
37+
* 点击 **Apply** 进行下载安装。
38+
39+
## 第四步:同意 Android 协议
40+
41+
打开 PowerShell 或 CMD,运行以下命令并一路输入 `y` 同意所有协议:
42+
43+
```bash
44+
flutter doctor --android-licenses
45+
```
46+
47+
## 第五步:配置 VS Code
48+
49+
1. 打开 VS Code。
50+
2. 点击左侧扩展图标 (Extensions)。
51+
3. 搜索并安装 **Flutter** 插件 (Dart 插件会自动安装)。
52+
53+
## 第六步:最终检查
54+
55+
在终端运行以下命令检查环境状态:
56+
57+
```bash
58+
flutter doctor
59+
```
60+
61+
如果所有项目前面都是绿色的勾(Visual Studio for Windows 可以忽略,那是用于开发 Windows 桌面应用的),则环境搭建完成!
62+
63+
---
64+
65+
## 如何运行本项目
66+
67+
1. 在 VS Code 中打开 `punch_card_record` 文件夹。
68+
2. 打开终端 (Terminal > New Terminal)。
69+
3. 下载项目依赖:
70+
```bash
71+
flutter pub get
72+
```
73+
4. 连接您的安卓手机(需开启 USB 调试)或启动 Android 模拟器。
74+
5. 运行应用:
75+
```bash
76+
flutter run

GITHUB_BUILD_GUIDE.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# 如何使用 GitHub 免费云端打包 APK (无需本地安装环境)
2+
3+
这是最简单的打包方法。您不需要在电脑上安装任何复杂的软件,只需要有一个 GitHub 账号。
4+
5+
## 第一步:准备代码
6+
7+
您现在的 `punch_card_record` 文件夹中已经包含了所有必要的代码和自动化配置(我刚刚添加了 `.github/workflows/build_apk.yml` 文件)。
8+
9+
## 第二步:上传到 GitHub
10+
11+
1. 登录 [GitHub](https://github.com/)
12+
2. 点击右上角的 **+** 号,选择 **New repository** (新建仓库)。
13+
3. 输入仓库名称(例如 `punch-card-app`),选择 **Public** (公开) 或 **Private** (私有) 均可。
14+
4. 点击 **Create repository**
15+
5. 在页面中找到 "uploading an existing file" (上传现有文件) 的链接,或者使用 GitHub Desktop 工具将 `punch_card_record` 文件夹中的**所有文件**上传到这个新仓库中。
16+
* **注意**:确保 `.github` 文件夹及其内部的 `workflows/build_apk.yml` 文件也被成功上传。
17+
18+
## 第三步:等待自动打包
19+
20+
1. 上传完成后,点击仓库顶部的 **Actions** 标签页。
21+
2. 您会看到一个名为 "Build Android APK" 的任务正在运行(黄色旋转图标)。
22+
3. 等待约 3-5 分钟,直到图标变成绿色的对勾 ✅。
23+
24+
## 第四步:下载 APK
25+
26+
1. 点击那个绿色的任务记录(通常显示为 "Initial commit" 或您提交时的备注)。
27+
2. 在任务详情页面的底部,找到 **Artifacts** 区域。
28+
3. 点击 **punch-card-record-apk**
29+
4. GitHub 会下载一个 `.zip` 压缩包。
30+
5. 解压该压缩包,里面就是您的安卓安装包 `app-release.apk`
31+
32+
---
33+
34+
**常见问题:**
35+
* **安装失败?** 由于是未签名的测试包,安装时手机可能会提示“未知来源”或“风险应用”,请选择“继续安装”或“信任此应用”即可。

lib/main.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:provider/provider.dart';
3+
import 'providers/punch_provider.dart';
4+
import 'screens/home_page.dart';
5+
6+
void main() {
7+
runApp(const MyApp());
8+
}
9+
10+
class MyApp extends StatelessWidget {
11+
const MyApp({super.key});
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
return MultiProvider(
16+
providers: [
17+
ChangeNotifierProvider(create: (_) => PunchProvider()),
18+
],
19+
child: MaterialApp(
20+
title: '打卡记',
21+
theme: ThemeData(
22+
primarySwatch: Colors.blue,
23+
useMaterial3: true,
24+
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
25+
),
26+
home: const HomePage(),
27+
),
28+
);
29+
}
30+
}

lib/models/punch_log.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class PunchLog {
2+
final String id;
3+
final DateTime timestamp;
4+
final String action; // e.g., "Punch +1", "Punch -1", "Set Name"
5+
final String details;
6+
bool isDeleted; // Soft delete for UI
7+
8+
PunchLog({
9+
required this.id,
10+
required this.timestamp,
11+
required this.action,
12+
required this.details,
13+
this.isDeleted = false,
14+
});
15+
16+
Map<String, dynamic> toJson() {
17+
return {
18+
'id': id,
19+
'timestamp': timestamp.toIso8601String(),
20+
'action': action,
21+
'details': details,
22+
'isDeleted': isDeleted,
23+
};
24+
}
25+
26+
factory PunchLog.fromJson(Map<String, dynamic> json) {
27+
return PunchLog(
28+
id: json['id'] as String,
29+
timestamp: DateTime.parse(json['timestamp']),
30+
action: json['action'] as String,
31+
details: json['details'] as String,
32+
isDeleted: json['isDeleted'] as bool? ?? false,
33+
);
34+
}
35+
}

lib/models/punch_record.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class PunchRecord {
2+
final DateTime date;
3+
final int count;
4+
5+
PunchRecord({required this.date, required this.count});
6+
7+
// Convert to JSON
8+
Map<String, dynamic> toJson() {
9+
return {
10+
'date': date.toIso8601String(),
11+
'count': count,
12+
};
13+
}
14+
15+
// Create from JSON
16+
factory PunchRecord.fromJson(Map<String, dynamic> json) {
17+
return PunchRecord(
18+
date: DateTime.parse(json['date']),
19+
count: json['count'] as int,
20+
);
21+
}
22+
}

lib/providers/punch_provider.dart

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
import 'package:uuid/uuid.dart';
4+
import '../models/punch_record.dart';
5+
import '../models/punch_log.dart';
6+
import '../services/storage_service.dart';
7+
8+
class PunchProvider with ChangeNotifier {
9+
final StorageService _storageService = StorageService();
10+
final Uuid _uuid = const Uuid();
11+
12+
List<PunchRecord> _records = [];
13+
List<PunchLog> _logs = [];
14+
15+
String _punchName = "每日打卡";
16+
int _idealIntervalDays = 1;
17+
18+
List<PunchRecord> get records => _records;
19+
List<PunchLog> get logs => _logs;
20+
String get punchName => _punchName;
21+
int get idealIntervalDays => _idealIntervalDays;
22+
23+
bool _isLoading = true;
24+
bool get isLoading => _isLoading;
25+
26+
PunchProvider() {
27+
_loadData();
28+
}
29+
30+
Future<void> _loadData() async {
31+
_isLoading = true;
32+
notifyListeners();
33+
34+
// Load Settings
35+
final prefs = await SharedPreferences.getInstance();
36+
_punchName = prefs.getString('punchName') ?? "每日打卡";
37+
_idealIntervalDays = prefs.getInt('idealIntervalDays') ?? 1;
38+
39+
// Load Data
40+
_records = await _storageService.loadRecords();
41+
_logs = await _storageService.loadLogs();
42+
43+
_isLoading = false;
44+
notifyListeners();
45+
}
46+
47+
// --- Settings ---
48+
49+
Future<void> updateSettings(String name, int interval) async {
50+
_punchName = name;
51+
_idealIntervalDays = interval;
52+
53+
final prefs = await SharedPreferences.getInstance();
54+
await prefs.setString('punchName', name);
55+
await prefs.setInt('idealIntervalDays', interval);
56+
57+
_addLog("设置更新", "名称: $name, 间隔: $interval 天");
58+
notifyListeners();
59+
}
60+
61+
// --- Punch Logic ---
62+
63+
int getCountForDate(DateTime date) {
64+
final record = _records.firstWhere(
65+
(r) => isSameDay(r.date, date),
66+
orElse: () => PunchRecord(date: date, count: 0),
67+
);
68+
return record.count;
69+
}
70+
71+
Future<void> updatePunch(DateTime date, int change) async {
72+
final index = _records.indexWhere((r) => isSameDay(r.date, date));
73+
int newCount;
74+
75+
if (index != -1) {
76+
newCount = _records[index].count + change;
77+
if (newCount < 0) newCount = 0; // Prevent negative
78+
79+
if (newCount == 0) {
80+
_records.removeAt(index);
81+
} else {
82+
_records[index] = PunchRecord(date: date, count: newCount);
83+
}
84+
} else {
85+
newCount = change > 0 ? change : 0;
86+
if (newCount > 0) {
87+
_records.add(PunchRecord(date: date, count: newCount));
88+
}
89+
}
90+
91+
await _storageService.saveRecords(_records);
92+
93+
String action = change > 0 ? "打卡 +$change" : "打卡 $change";
94+
_addLog(action, "日期: ${date.toString().split(' ')[0]}, 新数量: $newCount");
95+
96+
notifyListeners();
97+
}
98+
99+
// --- Log Logic ---
100+
101+
void _addLog(String action, String details) {
102+
final log = PunchLog(
103+
id: _uuid.v4(),
104+
timestamp: DateTime.now(),
105+
action: action,
106+
details: details,
107+
);
108+
_logs.insert(0, log); // Add to top
109+
_storageService.saveLogs(_logs);
110+
}
111+
112+
Future<void> softDeleteLog(String id) async {
113+
final index = _logs.indexWhere((l) => l.id == id);
114+
if (index != -1) {
115+
_logs[index].isDeleted = true;
116+
// We don't save the 'isDeleted' state to file to keep history intact in file,
117+
// but requirement says "store in file but delete from surface".
118+
// So we actually need to save the isDeleted state to file so it persists across restarts.
119+
await _storageService.saveLogs(_logs);
120+
notifyListeners();
121+
}
122+
}
123+
124+
// --- Helpers ---
125+
126+
DateTime? getLastPunchDate() {
127+
if (_records.isEmpty) return null;
128+
// Sort records by date descending
129+
final sorted = List<PunchRecord>.from(_records)
130+
..sort((a, b) => b.date.compareTo(a.date));
131+
return sorted.first.date;
132+
}
133+
134+
DateTime? getNextIdealDate() {
135+
final last = getLastPunchDate();
136+
if (last == null) return null;
137+
return last.add(Duration(days: _idealIntervalDays));
138+
}
139+
140+
bool isSameDay(DateTime? a, DateTime? b) {
141+
if (a == null || b == null) return false;
142+
return a.year == b.year && a.month == b.month && a.day == b.day;
143+
}
144+
}

0 commit comments

Comments
 (0)