Compare commits

...

No commits in common. 'main' and 'release' have entirely different histories.

60
.gitignore vendored

@ -1,60 +0,0 @@
# 构建输出
build/
target/
*.class
*.jar
*.war
*.ear
# Gradle
.gradle/
gradle-app.setting
!gradle-wrapper.jar
# Maven
.mvn/
!.mvn/wrapper/maven-wrapper.jar
# Android
*.apk
*.ap_
*.dex
local.properties
.idea/
*.iml
.DS_Store
/captures
.externalNativeBuild
.cxx
# 备份文件
*.bak
*.tmp
*.swp
*~
# 日志文件
*.log
# 临时文件
test_report.txt
*.png
*.jpg
!docs/**/*.png
!docs/**/*.jpg
# IDE
.vscode/
.settings/
.classpath
.project
*.iws
*.ipr
# 操作系统
Thumbs.db
.DS_Store
# 数据库备份
*.db.backup
*.db-journal

@ -1,332 +0,0 @@
# Design Document
## Overview
本设计文档描述了SLMS项目GUI应用打包的完整解决方案。当前问题是Jenkins流水线中并行打包后文件重命名步骤没有正确执行导致最终制品缺失。本设计将分两个阶段解决
**阶段1** 修复当前的文件复制问题确保JAR/WAR文件能正确重命名和归档
**阶段2** 实现jpackage打包生成EXE和MSI安装包
## Architecture
### 当前架构问题
```
并行打包阶段:
├─ CLI打包 → mvn package -Pcli → 生成 *-cli-shaded.jar
├─ GUI打包 → mvn package -Pgui-swing → 生成 *-gui-swing.jar
├─ Web打包 → mvn package -Pweb → 生成 *.war
└─ Android打包 → gradlew assembleDebug → 生成 *.apk
问题:
1. 并行执行时bat脚本的echo输出被混在一起
2. 文件复制命令可能执行了但输出不可见
3. 或者由于并行冲突,复制命令根本没执行
```
### 目标架构
```
阶段1 - 修复文件复制:
├─ 串行化关键步骤:打包完成后统一重命名
├─ 添加详细的文件检查和日志
├─ 使用PowerShell脚本替代bat以获得更好的错误处理
阶段2 - 添加jpackage:
├─ 检查环境依赖JDK 14+, WiX Toolset
├─ 生成jpackage输入目录
├─ 执行jpackage创建EXE和MSI
├─ 验证生成的文件
└─ 如果失败回退到JAR方案
```
## Components and Interfaces
### Component 1: 文件重命名模块
**职责:** 在所有并行打包完成后,统一检查和重命名文件
**接口:**
```groovy
stage('7.5 重命名制品') {
steps {
script {
renameArtifacts()
}
}
}
def renameArtifacts() {
// 检查并重命名CLI JAR
// 检查并重命名GUI JAR
// 检查并重命名Web WAR
// 复制library.db
// 生成README文件
}
```
### Component 2: jpackage打包模块
**职责:** 使用jpackage创建Windows原生安装包
**接口:**
```groovy
stage('7.6 创建Windows安装包') {
when {
expression { isJpackageAvailable() }
}
steps {
script {
createWindowsInstallers()
}
}
}
def isJpackageAvailable() {
// 检查jpackage命令
// 检查WiX Toolset
return true/false
}
def createWindowsInstallers() {
// 准备jpackage输入目录
// 执行jpackage --type app-image
// 执行jpackage --type msi
// 验证生成的文件
}
```
### Component 3: 环境检查模块
**职责:** 检查Jenkins环境是否满足jpackage要求
**接口:**
```groovy
def checkJpackageEnvironment() {
return [
jpackageAvailable: boolean,
jpackageVersion: string,
wixInstalled: boolean,
wixHome: string,
jdkVersion: string
]
}
```
## Data Models
### 制品文件模型
```
Artifacts:
CLI:
- smart-library-management-system-1.0-SNAPSHOT-cli-shaded.jar (源文件)
- slms-cli.jar (重命名后)
GUI:
- smart-library-management-system-1.0-SNAPSHOT-gui-swing.jar (源文件)
- slms-gui.jar (重命名后)
- slms-gui.exe (jpackage生成)
- slms-gui-installer.msi (jpackage生成)
- run-gui.bat (启动脚本)
- README-GUI.txt (说明文档)
Web:
- smart-library-management-system-1.0-SNAPSHOT.war (源文件)
- slms-web.war (重命名后)
Android:
- SLMS-debug.apk (源文件)
- slms-debug.apk (重命名后)
Database:
- library.db
```
### jpackage配置模型
```
JpackageConfig:
input: target/jpackage-input/
mainJar: slms-gui.jar
mainClass: com.smartlibrary.gui.SimpleGUIApplication
name: SLMS
appVersion: 1.0
vendor: CHZU
icon: resources/icon.ico
type: [app-image, msi]
winMenu: true
winDirChooser: true
winShortcut: true
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: 文件重命名完整性
*For any* 成功的打包构建,所有源文件(*-cli-shaded.jar, *-gui-swing.jar, *.war, *-debug.apk都应该被重命名为标准名称slms-cli.jar, slms-gui.jar, slms-web.war, slms-debug.apk
**Validates: Requirements 1.1, 1.2, 1.3**
### Property 2: 制品归档一致性
*For any* 归档操作Jenkins archiveArtifacts应该包含所有重命名后的文件且文件大小应该大于0
**Validates: Requirements 4.1, 4.2**
### Property 3: jpackage环境检查
*For any* jpackage打包尝试如果WiX Toolset不可用系统应该跳过EXE/MSI生成并记录警告而不是失败
**Validates: Requirements 5.1, 5.2, 5.3**
### Property 4: EXE可执行性
*For any* 生成的EXE文件双击应该能够启动应用并显示GUI界面
**Validates: Requirements 2.3**
### Property 5: MSI安装完整性
*For any* 生成的MSI文件安装后应该在开始菜单创建快捷方式且快捷方式能够启动应用
**Validates: Requirements 2.4, 2.5**
### Property 6: 制品推送完整性
*For any* 推送到release分支的操作artifacts目录应该包含所有可用的制品文件
**Validates: Requirements 4.5**
## Error Handling
### 错误场景1文件重命名失败
**检测:** 检查源文件是否存在
**处理:** 记录详细错误信息标记构建为UNSTABLE继续后续步骤
**恢复:** 在归档阶段尝试归档原始文件名
### 错误场景2jpackage不可用
**检测:** 执行`jpackage --version`检查返回码
**处理:** 记录警告信息跳过EXE/MSI生成使用JAR作为备用
**恢复:** 在README中说明只有JAR可用
### 错误场景3WiX Toolset未安装
**检测:** 检查WIX_HOME环境变量和candle.exe文件
**处理:** 记录详细的安装指导跳过MSI生成只生成EXE
**恢复:** 提供WiX安装链接和配置说明
### 错误场景4jpackage执行失败
**检测:** 检查jpackage命令的退出码
**处理:** 记录完整的错误输出保留JAR文件
**恢复:** 标记构建为UNSTABLE在邮件中说明情况
### 错误场景5制品推送失败
**检测:** 检查git push的返回码
**处理:** 记录错误信息但不影响Jenkins归档
**恢复:** 用户仍可从Jenkins下载制品
## Testing Strategy
### Unit Tests
由于这是CI/CD流水线配置主要通过集成测试验证
1. **本地测试**
- 在本地Windows环境执行jpackage命令
- 验证生成的EXE和MSI
- 测试安装和运行流程
2. **Jenkins测试**
- 触发Jenkins构建
- 检查构建日志
- 验证归档的制品
- 测试从release分支下载
### Integration Tests
1. **端到端测试**
- 完整的Jenkins流水线执行
- 验证所有阶段都成功
- 检查所有制品都被归档
- 验证release分支更新
2. **回退测试**
- 模拟jpackage不可用的情况
- 验证系统回退到JAR方案
- 确认构建标记为UNSTABLE而非FAILURE
### Manual Tests
1. **EXE测试**
- 下载slms-gui.exe
- 双击运行
- 验证GUI界面显示
- 测试所有功能(图书管理、借阅管理)
2. **MSI测试**
- 下载slms-gui-installer.msi
- 运行安装程序
- 验证安装到指定目录
- 检查开始菜单快捷方式
- 从快捷方式启动应用
- 测试所有功能
3. **卸载测试**
- 从控制面板卸载应用
- 验证文件被删除
- 验证快捷方式被删除
## Implementation Notes
### 阶段1实现要点
1. **添加独立的重命名阶段**
- 在并行打包后添加串行的重命名阶段
- 使用PowerShell脚本以获得更好的错误处理
- 添加详细的文件检查和日志输出
2. **改进日志输出**
- 为每个操作添加明确的开始/结束标记
- 使用不同的前缀区分不同的任务
- 在关键步骤后添加文件列表输出
3. **增强错误检查**
- 检查每个源文件是否存在
- 检查复制操作是否成功
- 记录文件大小以验证完整性
### 阶段2实现要点
1. **环境准备**
- 在Jenkins Agent上安装WiX Toolset 3.11
- 配置WIX_HOME环境变量
- 验证jpackage命令可用JDK 14+
2. **jpackage输入准备**
- 创建jpackage-input目录
- 复制slms-gui.jar到input目录
- 复制library.db到input目录
- 准备应用图标icon.ico
3. **jpackage执行**
- 首先创建app-image包含JRE的应用目录
- 然后基于app-image创建MSI安装包
- 验证生成的文件存在且大小合理
4. **回退机制**
- 如果jpackage失败保留JAR文件
- 在README中说明可用的格式
- 标记构建为UNSTABLE而非FAILURE
### 发布仓库配置
- 源代码推送到https://bdgit.educoder.net/pu6zrsfoy/CHZU_CS231_SEB_lab.git (main分支)
- 制品推送到https://bdgit.educoder.net/pu6zrsfoy/slms.git (release分支)

@ -1,95 +0,0 @@
# Requirements Document
## Introduction
本需求文档定义了SLMS项目GUI应用的打包需求。当前Jenkins流水线使用gui-swing profile生成JAR文件但用户要求生成EXE和MSI格式的Windows原生安装包。本地打包可以成功但Jenkins流水线中失败。需要分析根本原因并实现可靠的jpackage打包方案。
## Glossary
- **jpackage**: Java 14+引入的打包工具,用于创建平台特定的安装包
- **WiX Toolset**: Windows Installer XML工具集jpackage在Windows上创建MSI需要此工具
- **gui-swing profile**: Maven profile生成包含所有依赖的独立JAR文件
- **Shaded JAR**: 使用maven-shade-plugin打包的包含所有依赖的JAR文件
- **Jenkins Agent**: 执行Jenkins构建任务的节点环境
- **本地环境**: 开发者本地Windows机器
- **制品 (Artifacts)**: 构建产生的可分发文件JAR, EXE, MSI, APK等
## Requirements
### Requirement 1: 环境差异分析
**User Story:** 作为DevOps工程师我想要了解本地环境和Jenkins环境的差异以便找出打包失败的根本原因。
#### Acceptance Criteria
1. WHEN 分析本地环境配置 THEN 系统SHALL记录JDK版本、WiX Toolset版本、环境变量配置
2. WHEN 分析Jenkins环境配置 THEN 系统SHALL记录Jenkins Agent的JDK版本、WiX Toolset安装状态、环境变量配置
3. WHEN 对比两个环境 THEN 系统SHALL识别所有关键差异点
4. WHEN 识别到WiX Toolset缺失 THEN 系统SHALL记录WIX_HOME环境变量状态
5. WHEN 识别到jpackage不可用 THEN 系统SHALL记录jpackage命令的可用性和版本
### Requirement 2: jpackage打包实现
**User Story:** 作为用户我想要获得EXE和MSI格式的GUI应用安装包以便在Windows上方便地安装和运行应用。
#### Acceptance Criteria
1. WHEN 执行GUI打包 THEN 系统SHALL使用jpackage创建Windows EXE可执行文件
2. WHEN 执行GUI打包 THEN 系统SHALL使用jpackage创建Windows MSI安装包
3. WHEN 双击EXE文件 THEN 应用SHALL正常启动并显示GUI界面
4. WHEN 运行MSI安装程序 THEN 系统SHALL将应用安装到指定目录
5. WHEN MSI安装完成后 THEN 用户SHALL能够从开始菜单或桌面快捷方式启动应用
6. WHEN 应用启动 THEN 系统SHALL加载library.db数据库文件
7. WHEN 应用运行 THEN 所有GUI功能图书管理、借阅管理SHALL正常工作
### Requirement 3: Jenkins流水线集成
**User Story:** 作为DevOps工程师我想要在Jenkins流水线中自动化GUI打包过程以便每次构建都能生成完整的制品。
#### Acceptance Criteria
1. WHEN Jenkins流水线执行到GUI打包阶段 THEN 系统SHALL检查jpackage工具的可用性
2. WHEN jpackage不可用 THEN 系统SHALL检查WiX Toolset的安装状态
3. WHEN WiX Toolset未安装 THEN 系统SHALL提供清晰的错误信息和安装指导
4. WHEN 所有依赖满足 THEN 系统SHALL成功执行jpackage打包
5. WHEN 打包完成 THEN 系统SHALL验证生成的EXE和MSI文件存在且大小合理
6. WHEN 打包失败 THEN 系统SHALL记录详细的错误日志并标记构建为失败
### Requirement 4: 制品归档和发布
**User Story:** 作为用户我想要所有构建制品CLI JAR、GUI JAR/EXE/MSI、Web WAR、Android APK都被正确归档和发布到头歌release分支以便下载使用。
#### Acceptance Criteria
1. WHEN 所有打包任务完成 THEN 系统SHALL归档以下制品slms-cli.jar, slms-gui.jar, slms-gui.exe, slms-gui-installer.msi, slms-web.war, slms-debug.apk, library.db
2. WHEN 归档制品 THEN 系统SHALL使用Jenkins archiveArtifacts功能保存制品
3. WHEN 推送到头歌 THEN 系统SHALL将所有制品复制到artifacts目录
4. WHEN 推送到头歌 THEN 系统SHALL创建包含构建信息的README.md文件
5. WHEN 推送到头歌 THEN 系统SHALL将artifacts目录推送到release分支
6. WHEN 推送完成 THEN 用户SHALL能够从头歌release分支下载所有制品
### Requirement 5: 错误处理和回退机制
**User Story:** 作为DevOps工程师我想要在jpackage打包失败时有合理的回退机制以便构建不会完全失败。
#### Acceptance Criteria
1. WHEN jpackage打包失败 THEN 系统SHALL记录详细错误信息
2. WHEN jpackage打包失败 THEN 系统SHALL继续生成gui-swing JAR作为备用
3. WHEN 使用备用方案 THEN 系统SHALL在构建日志中明确标注
4. WHEN 使用备用方案 THEN 系统SHALL将构建状态标记为UNSTABLE而非FAILURE
5. WHEN 构建完成 THEN 系统SHALL在邮件通知中说明哪些制品可用
### Requirement 6: 文档和使用说明
**User Story:** 作为用户我想要清晰的文档说明如何使用不同格式的GUI应用以便选择最适合的方式运行应用。
#### Acceptance Criteria
1. WHEN 生成EXE文件 THEN 系统SHALL创建README-GUI-EXE.txt说明文件
2. WHEN 生成MSI文件 THEN 系统SHALL创建README-GUI-MSI.txt说明文件
3. WHEN 生成JAR文件 THEN 系统SHALL创建README-GUI-JAR.txt说明文件
4. WHEN 创建README文件 THEN 文档SHALL包含系统要求、安装步骤、运行方法
5. WHEN 创建README文件 THEN 文档SHALL包含常见问题和故障排除指南
6. WHEN 推送到release分支 THEN 系统SHALL包含所有README文件

@ -1,173 +0,0 @@
# Implementation Plan
## 阶段1修复并行打包中的文件重命名问题保持并行
- [x] 1. 修复并行打包阶段的文件复制逻辑
- **保持并行执行,不改为串行**
- 在每个并行任务内部,确保文件复制命令正确执行
- 添加详细的调试输出以追踪问题
- _Requirements: 1.1, 1.2, 1.3, 4.1_
- [x] 1.1 修复CLI打包任务中的文件复制
- 在mvn package命令成功后立即执行文件复制
- 添加echo输出显示每个步骤
- 使用dir命令验证文件存在
- 复制smart-library-management-system-1.0-SNAPSHOT-cli-shaded.jar为slms-cli.jar
- 复制library.db到target目录
- _Requirements: 1.1_
- [x] 1.2 修复GUI打包任务中的文件复制
- 在mvn package命令成功后立即执行文件复制
- 添加echo输出显示每个步骤
- 复制smart-library-management-system-1.0-SNAPSHOT-gui-swing.jar为slms-gui.jar
- 创建run-gui.bat启动脚本
- 创建README-GUI.txt说明文档
- 复制library.db到target目录
- _Requirements: 1.2, 6.3_
- [x] 1.3 修复Web打包任务中的文件复制
- 在mvn package命令成功后立即执行文件复制
- 添加echo输出显示每个步骤
- 复制smart-library-management-system-1.0-SNAPSHOT.war为slms-web.war
- _Requirements: 1.3_
- [x] 1.4 改进并行任务的日志输出格式
- 为每个任务的echo添加唯一前缀[CLI], [GUI], [Web], [Android]
- 在关键步骤前后添加分隔线
- 确保每个步骤都有明确的输出
- _Requirements: 3.5_
- [x] 1.5 添加文件验证步骤
- 在每个并行任务结束前,列出生成的文件
- 显示文件大小
- 如果关键文件缺失输出错误信息并exit /b 1
- _Requirements: 3.5, 3.6_
- [x] 2. 修复发布仓库地址
- 将release分支推送地址从CHZU_CS231_SEB_lab.git改为slms.git
- 保持main分支推送到CHZU_CS231_SEB_lab.git
- 更新所有相关的git remote和git push命令
- _Requirements: 4.5_
- [ ] 3. 测试阶段1修复
- 触发Jenkins构建
- 检查构建日志,确认所有文件复制步骤都有输出
- 验证target目录包含slms-cli.jar, slms-gui.jar, slms-web.war
- 验证归档包含所有制品
- 验证release分支更新成功
- _Requirements: 4.1, 4.2, 4.5_
## 阶段2实现jpackage打包在阶段1成功后
- [ ] 4. 添加环境检查阶段
- 创建新的stage检查jpackage和WiX Toolset
- 记录环境信息到构建日志
- 如果环境不满足跳过jpackage打包
- _Requirements: 1.1, 1.2, 1.4, 1.5_
- [ ] 4.1 实现jpackage可用性检查
- 执行jpackage --version命令
- 解析版本号需要JDK 14+
- 如果失败,记录警告并设置标志跳过后续步骤
- _Requirements: 3.1, 3.2_
- [ ] 4.2 实现WiX Toolset检查
- 检查WIX_HOME环境变量
- 检查candle.exe文件是否存在
- 如果失败,记录安装指导链接
- _Requirements: 3.3, 3.4_
- [ ] 5. 实现jpackage打包阶段
- 创建新的stage执行jpackage打包
- 仅在jpackage和WiX都可用时执行
- 使用when条件控制执行
- _Requirements: 2.1, 2.2_
- [ ] 5.1 准备jpackage输入目录
- 创建target/jpackage-input目录
- 复制slms-gui.jar到input目录
- 复制library.db到input目录
- 如果存在应用图标复制到input目录
- _Requirements: 2.1_
- [ ] 5.2 创建EXE可执行文件
- 执行jpackage --type app-image命令
- 指定--input, --main-jar, --main-class参数
- 设置--name, --app-version, --vendor参数
- 验证生成的EXE文件存在
- _Requirements: 2.1, 2.3_
- [ ] 5.3 创建MSI安装包
- 执行jpackage --type msi命令
- 基于app-image创建MSI
- 启用--win-menu, --win-dir-chooser, --win-shortcut选项
- 验证生成的MSI文件存在
- _Requirements: 2.2, 2.4, 2.5_
- [ ] 5.4 重命名和复制jpackage生成的文件
- 将生成的EXE重命名为slms-gui.exe
- 将生成的MSI重命名为slms-gui-installer.msi
- 复制到target目录以便归档
- _Requirements: 2.1, 2.2_
- [ ] 5.5 创建EXE和MSI的README文档
- 创建README-GUI-EXE.txt说明EXE使用方法
- 创建README-GUI-MSI.txt说明MSI安装步骤
- 包含系统要求、运行方法、故障排除
- _Requirements: 6.1, 6.2, 6.4_
- [ ] 6. 实现错误处理和回退机制
- 使用try-catch包装jpackage执行
- 如果jpackage失败记录详细错误信息
- 标记构建为UNSTABLE而非FAILURE
- 确保JAR文件仍然可用作备用
- _Requirements: 5.1, 5.2, 5.3, 5.4_
- [ ] 7. 更新归档和发布配置
- 更新archiveArtifacts包含*.exe和*.msi
- 更新stash包含EXE和MSI文件
- 更新release分支README说明EXE和MSI
- _Requirements: 4.1, 4.2, 4.4_
- [ ] 8. Jenkins环境配置
- 在Jenkins Agent上安装WiX Toolset 3.11
- 下载地址https://github.com/wixtoolset/wix3/releases
- 配置WIX_HOME环境变量指向安装目录
- 重启Jenkins服务使环境变量生效
- 验证jpackage和WiX都可用
- _Requirements: 3.1, 3.2, 3.3_
- [ ] 9. 本地测试jpackage
- 在本地Windows环境测试jpackage命令
- 验证生成的EXE可以双击运行
- 验证MSI安装程序可以正常安装
- 验证安装后的应用可以从开始菜单启动
- 测试所有GUI功能图书管理、借阅管理
- _Requirements: 2.3, 2.4, 2.5, 2.7_
- [ ] 10. Jenkins端到端测试
- 触发完整的Jenkins构建
- 验证所有制品都被生成JAR, EXE, MSI, WAR, APK
- 从Jenkins下载制品并测试
- 从release分支下载制品并测试
- 验证邮件通知包含所有制品信息
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 4.5_
- [ ] 11. 文档更新
- 更新项目README说明新的制品格式
- 创建jpackage故障排除指南
- 记录Jenkins环境配置步骤
- 创建用户使用指南如何选择JAR/EXE/MSI
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6_

@ -1,496 +0,0 @@
# Design Document
## Overview
本设计文档描述了 SLMS 项目仓库重构和 CI/CD 流水线优化的技术方案。主要目标是:
1. 简化仓库结构,移除不必要的子目录嵌套
2. 优化 Gitea 代码拉取速度,从几十分钟降低到 2 分钟以内
3. 修改 Jenkinsfile 以适配新的仓库结构
4. 统一仓库命名为 `slms`
5. 提供完整的仓库迁移指南
## Architecture
### 当前架构问题
```
当前仓库结构(问题):
某个父目录/
└── SLMS/ # 实际项目根目录
├── android/
├── backend/
├── src/
├── pom.xml
├── Jenkinsfile # 使用 dir('SLMS') 切换目录
└── ...
Jenkinsfile 执行流程:
1. checkout scm # 拉取到 workspace
2. dir('SLMS') { ... } # 切换到 SLMS 子目录
3. mvn commands # 在子目录执行构建
```
### 目标架构
```
优化后的仓库结构:
slms/ # 仓库根目录即项目根目录
├── android/
├── backend/
├── src/
├── pom.xml
├── Jenkinsfile # 直接在根目录操作
└── ...
Jenkinsfile 执行流程:
1. checkout scm # 拉取到 workspace 根目录
2. mvn commands # 直接在根目录执行构建
3. 无需 dir() 切换目录
```
### 仓库配置
```
双仓库策略:
1. 本地 Gitea (开发/CI):
- URL: http://localhost:3000/gitea/slms.git
- 用途: Jenkins CI/CD 拉取源码
- 特点: 本地网络,速度快
2. 头歌仓库 (发布):
- URL: https://bdgit.educoder.net/pu6zrsfoy/slms.git
- 用途: 代码发布和制品归档
- 特点: 外部平台,需要优化推送策略
```
## Components and Interfaces
### 1. Git 配置优化组件
**功能**: 优化 Git 拉取性能
**配置参数**:
```groovy
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
extensions: [
[$class: 'CloneOption',
depth: 1, // 浅克隆,只拉取最近 1 次提交
noTags: true, // 不拉取标签
shallow: true], // 启用浅克隆
[$class: 'CleanBeforeCheckout'], // 拉取前清理
[$class: 'CleanCheckout'] // 拉取后清理
],
userRemoteConfigs: [[
url: 'http://localhost:3000/gitea/slms.git',
credentialsId: 'gitea-credentials'
]]
])
```
**Git 全局配置**:
```bash
git config --global http.postBuffer 524288000 # 500MB 缓冲区
git config --global http.lowSpeedLimit 1000 # 最低速度 1KB/s
git config --global http.lowSpeedTime 60 # 超时 60 秒
git config --global core.compression 0 # 禁用压缩加快传输
```
### 2. Jenkinsfile 重构组件
**修改策略**:
- 移除所有 `dir('SLMS')` 调用
- 更新所有路径引用为相对于根目录
- 优化 checkout 配置
- 更新制品归档路径
**关键修改点**:
| 阶段 | 当前路径 | 优化后路径 |
|------|---------|-----------|
| Maven 编译 | `dir('SLMS') { mvn ... }` | `mvn ...` |
| 测试报告 | `**/SLMS/target/surefire-reports/*.xml` | `**/target/surefire-reports/*.xml` |
| 制品归档 | `SLMS/target/slms-*.jar` | `target/slms-*.jar` |
| APK 归档 | `SLMS/android/build/outputs/apk/debug/*.apk` | `android/build/outputs/apk/debug/*.apk` |
### 3. 双仓库推送组件
**功能**: 管理本地 Gitea 和头歌仓库的代码同步
**推送策略**:
```groovy
// 策略 1: 源代码推送到头歌 feature-ldl 分支
stage('推送源代码到头歌') {
steps {
withCredentials([usernamePassword(...)]) {
bat '''
git remote add educoder https://bdgit.educoder.net/pu6zrsfoy/slms.git ||
git remote set-url educoder https://bdgit.educoder.net/pu6zrsfoy/slms.git
git push educoder HEAD:refs/heads/feature-ldl --force
'''
}
}
}
// 策略 2: 制品推送到头歌 release 分支
stage('推送制品到头歌') {
steps {
// 创建临时目录,只包含制品
// 推送到 release 分支
}
}
```
### 4. 工作空间清理组件
**功能**: 清理被占用的文件和进程
**清理策略**:
```batch
@echo off
REM 1. 清理 Java 进程
for /f "tokens=2" %%i in ('tasklist /FI "IMAGENAME eq java.exe" /FO LIST ^| findstr /C:"PID:"') do (
wmic process where "ProcessId=%%i and CommandLine like '%%slms%%'" delete 2>nul
)
REM 2. 清理 Gradle 守护进程
gradlew --stop
REM 3. 清理临时文件
if exist target rmdir /S /Q target
if exist .gradle rmdir /S /Q .gradle
if exist android\build rmdir /S /Q android\build
```
## Data Models
### Jenkins 环境变量
```groovy
environment {
// 工具路径
JAVA_HOME = 'E:\\2025-2026\\GitAIOps\\jdk'
ANDROID_HOME = 'D:\\development\\Android'
WIX_HOME = 'C:\\Program Files (x86)\\WiX Toolset v3.11'
// SonarQube 配置
SONAR_HOST_URL = 'http://localhost:9000'
SONAR_PROJECT_KEY = 'slms' // 统一使用小写 slms
SONAR_PROJECT_NAME = 'slms' // 项目名称也使用 slms
// Git 配置
GIT_LOCAL_REPO = 'http://localhost:3000/gitea/slms.git'
GIT_EDUCODER_REPO = 'https://bdgit.educoder.net/pu6zrsfoy/slms.git'
}
```
### 制品路径映射
```yaml
CLI:
source: target/smart-library-management-system-1.0-SNAPSHOT-cli-shaded.jar
target: target/slms-cli.jar
archive: target/slms-cli.jar
GUI:
jar:
source: target/smart-library-management-system-1.0-SNAPSHOT.jar
target: target/slms-gui.jar
archive: target/slms-gui.jar
exe:
source: target/SLMS-GUI/SLMS-GUI.exe
target: target/slms-gui.exe
archive: target/slms-gui.exe
msi:
source: target/SLMS-GUI-1.0.msi
target: target/slms-gui-installer.msi
archive: target/slms-gui-installer.msi
Web:
war:
source: target/smart-library-management-system-1.0-SNAPSHOT.war
target: target/slms-web.war
archive: target/slms-web.war
Android:
apk:
source: android/build/outputs/apk/debug/SLMS-debug.apk
target: android/build/outputs/apk/debug/slms-debug.apk
archive: android/build/outputs/apk/debug/slms-debug.apk
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: 路径一致性
*For any* Jenkinsfile 执行阶段,所有文件路径引用应相对于仓库根目录,不应包含 `SLMS/` 前缀
**Validates: Requirements 1.3, 3.2, 3.5**
### Property 2: Git 拉取性能
*For any* Git 拉取操作,完成时间应小于 120 秒2 分钟)
**Validates: Requirements 2.4**
### Property 3: 制品路径正确性
*For any* 构建产物,归档路径应能够正确定位到生成的文件
**Validates: Requirements 3.4, 3.5**
### Property 4: 命名一致性
*For any* 仓库引用、项目名称、构建产物命名,应统一使用小写 `slms` 前缀
**Validates: Requirements 4.1, 4.2, 4.3**
### Property 5: 双仓库同步
*For any* 代码推送操作,应能够成功推送到本地 Gitea 和头歌仓库
**Validates: Requirements 5.3**
### Property 6: 进程清理完整性
*For any* 流水线执行,开始和结束时应清理所有相关 Java 进程,避免文件占用
**Validates: Requirements 6.1, 6.3**
## Error Handling
### Git 拉取失败处理
```groovy
stage('拉取代码') {
steps {
retry(3) { // 重试 3 次
timeout(time: 5, unit: 'MINUTES') { // 超时 5 分钟
script {
try {
checkout scm
} catch (Exception e) {
echo "拉取失败: ${e.message}"
// 清理并重试
bat 'git clean -fdx'
throw e
}
}
}
}
}
}
```
### 制品缺失处理
```groovy
stage('归档制品') {
steps {
script {
try {
archiveArtifacts artifacts: 'target/slms-*.jar,...',
allowEmptyArchive: true, // 允许部分制品缺失
onlyIfSuccessful: false // 即使构建失败也归档
} catch (Exception e) {
echo "归档警告: ${e.message}"
currentBuild.result = 'UNSTABLE'
}
}
}
}
```
### 推送失败处理
```groovy
stage('推送到头歌') {
steps {
script {
try {
// 推送代码
bat 'git push educoder ...'
} catch (Exception e) {
echo "推送失败: ${e.message}"
echo "可能原因: 网络问题、认证失败、冲突"
currentBuild.result = 'UNSTABLE' // 标记为不稳定但继续
}
}
}
}
```
## Testing Strategy
### 单元测试
本项目主要涉及配置和脚本修改,单元测试策略如下:
1. **Jenkinsfile 语法验证**
- 使用 Jenkins Pipeline Linter 验证语法
- 命令: `curl -X POST -F "jenkinsfile=<Jenkinsfile" http://localhost:8080/pipeline-model-converter/validate`
2. **路径引用测试**
- 在测试环境执行 Jenkinsfile
- 验证所有文件路径能够正确解析
- 检查制品是否生成在预期位置
3. **Git 配置测试**
- 测试浅克隆是否正常工作
- 测试拉取速度是否满足要求(< 2
- 测试双仓库推送是否成功
### 集成测试
1. **完整流水线测试**
- 在 Jenkins 中执行完整流水线
- 验证所有阶段都能成功执行
- 检查所有制品是否正确归档
2. **仓库迁移测试**
- 创建测试仓库验证迁移步骤
- 确认本地 Gitea 和头歌仓库都能正常访问
- 验证 Jenkins 能够从新仓库拉取代码
3. **性能测试**
- 测量 Git 拉取时间
- 测量完整流水线执行时间
- 对比优化前后的性能提升
### 验收测试
1. **Git 拉取速度验收**
- 标准: 从 Gitea 拉取代码时间 < 2
- 方法: 多次执行并记录平均时间
2. **流水线稳定性验收**
- 标准: 连续 5 次执行无文件占用错误
- 方法: 连续触发流水线并观察结果
3. **制品完整性验收**
- 标准: 所有 4 端制品CLI、GUI、Web、Android都能正确生成和归档
- 方法: 检查归档的制品列表
## Implementation Notes
### Jenkinsfile 修改清单
需要修改的主要部分:
1. **Stage 1: 拉取代码**
- 添加浅克隆配置
- 移除 `dir('SLMS')` 引用
2. **Stage 2: Maven 编译**
- 移除 `dir('SLMS')`
- 直接在根目录执行 `mvn` 命令
3. **Stage 3-7: 所有构建阶段**
- 移除所有 `dir('SLMS')` 调用
- 更新所有路径引用
4. **Stage 8: 归档制品**
- 更新制品路径: `SLMS/target/*``target/*`
- 更新 APK 路径: `SLMS/android/*``android/*`
5. **Stage 9: 推送头歌**
- 移除 `cd SLMS` 命令
- 更新相对路径引用
### 仓库迁移步骤
```bash
# 1. 在当前项目根目录初始化 Git如果还没有
git init
# 2. 添加所有文件
git add .
# 3. 创建初始提交
git commit -m "Initial commit: SLMS project restructure"
# 4. 添加本地 Gitea 远程仓库
git remote add origin http://localhost:3000/gitea/slms.git
# 5. 添加头歌远程仓库
git remote add educoder https://bdgit.educoder.net/pu6zrsfoy/slms.git
# 6. 推送到本地 Gitea主分支
git push -u origin main --force
# 7. 推送到头歌feature-ldl 分支)
git push educoder main:feature-ldl --force
# 8. 验证推送
git remote -v
```
### Jenkins 配置更新
1. **更新 SCM 配置**
- 仓库 URL: `http://localhost:3000/gitea/slms.git`
- 分支: `*/main`
- 添加浅克隆选项
2. **更新凭据**
- 确保 `gitea-credentials` 凭据存在
- 确保 `educoder-credentials` 凭据存在
3. **更新 Webhook**
- Gitea Webhook URL: `http://jenkins-server:8080/gitea-webhook/post`
- 触发事件: Push events
4. **更新 SonarQube 配置**
- 项目键: `slms`(不再使用 `slms:slms`
- 项目名称: `slms`
- 在 SonarQube 中创建或重命名项目
### Git 性能优化配置
在 Jenkins 节点上执行:
```bash
# 增加 HTTP 缓冲区
git config --global http.postBuffer 524288000
# 设置超时参数
git config --global http.lowSpeedLimit 1000
git config --global http.lowSpeedTime 60
# 禁用压缩(本地网络不需要)
git config --global core.compression 0
# 启用并行传输
git config --global http.maxRequests 10
```
### 大文件处理建议
如果仓库包含大文件(如 APK、JAR考虑
1. **使用 .gitignore 排除构建产物**
```gitignore
# 构建产物
target/
build/
*.apk
*.jar
*.war
*.exe
*.msi
# 保留示例文件
!docs/examples/*.jar
```
2. **使用 Git LFS可选**
```bash
git lfs install
git lfs track "*.apk"
git lfs track "*.jar"
git add .gitattributes
```
3. **定期清理历史**
```bash
# 清理大于 10MB 的文件历史
git filter-branch --tree-filter 'find . -size +10M -delete' HEAD
```

@ -1,88 +0,0 @@
# Requirements Document
## Introduction
本需求文档描述了 SLMS (Smart Library Management System) 项目的仓库重构和 CI/CD 流水线优化需求。当前项目存在仓库结构不合理、Gitea 拉取缓慢(达几十分钟)等问题,需要进行优化以提高开发和部署效率。
## Glossary
- **SLMS**: Smart Library Management System智能图书管理系统
- **Gitea**: 轻量级的 Git 代码托管服务
- **Jenkins**: 持续集成/持续部署 (CI/CD) 自动化服务器
- **Jenkinsfile**: Jenkins 流水线配置文件
- **Repository**: Git 代码仓库
- **Workspace**: Jenkins 工作空间目录
- **Artifact**: 构建产物JAR、WAR、APK 等)
- **Shallow Clone**: Git 浅克隆,只拉取最近的提交历史以加快速度
## Requirements
### Requirement 1
**User Story:** 作为开发人员,我希望优化仓库结构,使项目根目录直接包含源代码,而不是嵌套在子目录中,以便简化路径引用和提高构建效率。
#### Acceptance Criteria
1. WHEN 项目被克隆时 THEN 系统应将源代码直接放置在仓库根目录
2. WHEN 构建脚本执行时 THEN 系统应能够直接在根目录访问所有源文件和构建文件
3. WHEN Jenkinsfile 执行时 THEN 系统不应需要切换到子目录(如 `dir('SLMS')`)来执行构建命令
4. WHEN 开发人员查看仓库时 THEN 系统应展示清晰的项目结构所有主要组件android、backend、src 等)在根目录可见
### Requirement 2
**User Story:** 作为 DevOps 工程师,我希望优化 Gitea 代码拉取速度,将拉取时间从几十分钟降低到可接受的范围(少于 2 分钟),以便加快 CI/CD 流水线执行。
#### Acceptance Criteria
1. WHEN Jenkins 从 Gitea 拉取代码时 THEN 系统应使用浅克隆策略以减少传输数据量
2. WHEN 拉取操作执行时 THEN 系统应配置合理的 Git 缓冲区大小以提高传输效率
3. WHEN 网络连接不稳定时 THEN 系统应实施重试机制以确保拉取成功
4. WHEN 拉取完成时 THEN 系统应在 2 分钟内完成代码获取操作
5. WHEN 仓库包含大文件时 THEN 系统应使用 Git LFS 或排除策略来避免传输不必要的大文件
### Requirement 3
**User Story:** 作为 DevOps 工程师,我希望修改 Jenkinsfile 以适配新的仓库结构,确保所有构建阶段能够正确执行,以便实现自动化部署。
#### Acceptance Criteria
1. WHEN Jenkinsfile 执行拉取代码阶段时 THEN 系统应直接在工作空间根目录操作
2. WHEN Jenkinsfile 执行 Maven 编译阶段时 THEN 系统应在根目录执行 `mvn` 命令而不需要 `dir('SLMS')`
3. WHEN Jenkinsfile 执行测试阶段时 THEN 系统应正确定位测试报告路径
4. WHEN Jenkinsfile 执行打包阶段时 THEN 系统应正确生成和定位所有构建产物
5. WHEN Jenkinsfile 执行归档阶段时 THEN 系统应使用正确的相对路径归档制品
6. WHEN Jenkinsfile 执行推送阶段时 THEN 系统应从正确的目录推送代码和制品
### Requirement 4
**User Story:** 作为开发人员,我希望统一所有仓库引用名称为 `slms`,以便保持命名一致性和可维护性。
#### Acceptance Criteria
1. WHEN Jenkinsfile 引用项目名称时 THEN 系统应使用小写的 `slms` 而不是 `SLMS`
2. WHEN Git 远程仓库配置时 THEN 系统应使用 `slms` 作为仓库名称
3. WHEN 构建产物命名时 THEN 系统应使用 `slms-` 前缀(如 `slms-cli.jar`
4. WHEN SonarQube 项目配置时 THEN 系统应使用 `slms:slms` 作为项目键
### Requirement 5
**User Story:** 作为开发人员,我希望获得清晰的仓库迁移指南,包括如何创建新的本地仓库并推送到本地 Gitea 和头歌仓库,以便顺利完成仓库重构。
#### Acceptance Criteria
1. WHEN 开发人员需要创建新仓库时 THEN 系统应提供初始化 Git 仓库的命令
2. WHEN 开发人员需要配置远程仓库时 THEN 系统应提供添加本地 Gitea (`http://localhost:3000/gitea/slms.git`) 和头歌 (`https://bdgit.educoder.net/pu6zrsfoy/slms.git`) 远程地址的命令
3. WHEN 开发人员需要推送代码时 THEN 系统应提供推送到本地 Gitea 和头歌仓库的完整命令序列
4. WHEN 开发人员需要配置 Jenkins 时 THEN 系统应提供更新 Jenkins 仓库配置指向本地 Gitea 的说明
5. WHEN 迁移完成时 THEN 系统应提供验证步骤以确保新仓库在本地 Gitea 和头歌都正常工作
### Requirement 6
**User Story:** 作为 DevOps 工程师,我希望优化 Jenkins 工作空间清理策略,避免文件占用导致的构建失败,以便提高流水线稳定性。
#### Acceptance Criteria
1. WHEN 流水线开始时 THEN 系统应清理可能被占用的 Java 进程
2. WHEN 流水线执行时 THEN 系统应避免在构建过程中锁定文件
3. WHEN 流水线结束时 THEN 系统应清理所有临时文件和进程
4. WHEN 文件清理失败时 THEN 系统应记录警告但继续执行流水线

@ -1,184 +0,0 @@
# Implementation Plan
- [x] 1. 备份当前项目和配置
- 创建项目完整备份
- 导出 Jenkins 配置
- 导出 SonarQube 配置
- _Requirements: 所有需求的前置条件_
- [x] 2. 修改 Jenkinsfile - Stage 1 拉取代码优化
- 添加 Git 浅克隆配置depth: 1, noTags: true, shallow: true
- 添加 CleanBeforeCheckout 和 CleanCheckout 扩展
- 更新仓库 URL 为本地 Gitea: `http://localhost:3000/gitea/slms.git`
- 移除拉取阶段中的 `dir('SLMS')` 引用
- _Requirements: 2.1, 2.2, 3.1_
- [x] 3. 修改 Jenkinsfile - Stage 2 Maven 编译
- 移除 `dir('SLMS')` 包装
- 直接在根目录执行 Maven 命令
- 更新清理 target 目录的路径引用
- _Requirements: 1.3, 3.2_
- [x] 4. 修改 Jenkinsfile - Stage 3 运行测试
- 移除 `dir('SLMS')` 包装
- 更新测试报告路径: `**/SLMS/target/surefire-reports/*.xml``**/target/surefire-reports/*.xml`
- _Requirements: 3.3_
- [x] 5. 修改 Jenkinsfile - Stage 4 SonarQube 质检
- 移除 `dir('SLMS')` 包装
- 更新 SONAR_PROJECT_KEY 环境变量: `slms:slms``slms`
- 添加 SONAR_PROJECT_NAME 环境变量: `slms`
- _Requirements: 3.2, 4.4_
- [x] 6. 修改 Jenkinsfile - Stage 6 准备打包
- 移除 `dir('SLMS')` 包装
- 更新 target 目录复制命令的路径
- _Requirements: 3.2_
- [x] 7. 修改 Jenkinsfile - Stage 7.1 CLI 打包
- 移除 `dir('SLMS')` 包装
- 更新所有 target-cli 和 target 路径引用
- 确保 library.db 复制路径正确
- _Requirements: 3.2, 3.4_
- [x] 8. 修改 Jenkinsfile - Stage 7.2 GUI 打包
- 移除 `dir('SLMS')` 包装
- 更新所有 target-gui 和 target 路径引用
- 更新 jpackage 输入输出路径
- 更新 EXE 和 MSI 文件路径
- _Requirements: 3.2, 3.4_
- [x] 9. 修改 Jenkinsfile - Stage 7.3 Web 打包
- 移除 `dir('SLMS')` 包装
- 更新所有 target-web 和 target 路径引用
- _Requirements: 3.2, 3.4_
- [x] 10. 修改 Jenkinsfile - Stage 7.4 Android 打包
- 移除 `dir('SLMS')` 包装
- 更新 APK 路径引用
- 确保 gradlew 命令在正确目录执行
- _Requirements: 3.2, 3.4_
- [x] 11. 修改 Jenkinsfile - Stage 8 归档制品
- 更新制品归档路径: `SLMS/target/*``target/*`
- 更新 APK 归档路径: `SLMS/android/*``android/*`
- 移除 `dir SLMS` 命令
- _Requirements: 3.5_
- [x] 12. 修改 Jenkinsfile - Stage 9.1 推送源代码
- 移除路径切换命令
- 更新 Git 远程仓库 URL 为头歌: `https://bdgit.educoder.net/pu6zrsfoy/slms.git`
- 确保推送到 feature-ldl 分支
- _Requirements: 3.6, 4.2, 5.3_
- [x] 13. 修改 Jenkinsfile - Stage 9.2 推送制品
- 移除 `cd SLMS` 命令
- 更新所有相对路径引用target、android/build 等)
- 更新临时目录中的路径复制命令
- 确保推送到头歌 release 分支
- _Requirements: 3.6, 5.3_
- [x] 14. 修改 Jenkinsfile - Post 清理阶段
- 更新制品检查路径: `SLMS/target/*``target/*`
- 更新 APK 检查路径: `SLMS/android/*``android/*`
- 优化进程清理脚本,使用 `slms` 而不是 `SLMS`
- _Requirements: 6.1, 6.3, 6.4_
- [x] 15. 配置 Jenkins 节点 Git 性能优化
- 在 Jenkins 节点执行: `git config --global http.postBuffer 524288000`
- 在 Jenkins 节点执行: `git config --global http.lowSpeedLimit 1000`
- 在 Jenkins 节点执行: `git config --global http.lowSpeedTime 60`
- 在 Jenkins 节点执行: `git config --global core.compression 0`
- _Requirements: 2.2_
- [x] 16. 更新 SonarQube 项目配置
- 在 SonarQube 中创建或重命名项目为 `slms`
- 更新项目键为 `slms`(如果之前是 `slms:slms`
- 验证质量门禁配置
- _Requirements: 4.4_
- [x] 17. 创建新的 Git 仓库并推送到本地 Gitea
- 在项目根目录执行: `git init`(如果需要)
- 添加所有文件: `git add .`
- 创建初始提交: `git commit -m "Initial commit: SLMS project restructure"`
- 添加本地 Gitea 远程仓库: `git remote add origin http://localhost:3000/gitea/slms.git`
- 推送到本地 Gitea: `git push -u origin main --force`
- _Requirements: 5.1, 5.2, 5.3_
- [x] 18. 配置头歌远程仓库
- 添加头歌远程仓库: `git remote add educoder https://bdgit.educoder.net/pu6zrsfoy/slms.git`
- 推送到头歌 main 分支: `git push educoder main --force`
- 推送到头歌 feature-ldl 分支: `git push educoder main:feature-ldl --force`
- 验证远程仓库: `git remote -v`
- _Requirements: 5.2, 5.3_
- [x] 19. 更新 Jenkins 任务配置
- 更新 SCM 仓库 URL 为: `http://localhost:3000/gitea/slms.git`
- 配置分支为: `*/main`
- 添加浅克隆选项(如果 UI 支持)
- 验证 `gitea-credentials` 凭据存在且有效
- 验证 `educoder-credentials` 凭据存在且有效
- _Requirements: 5.4_
- [x] 20. 测试完整流水线
- 在 Jenkins 中手动触发流水线
- 验证代码拉取时间 < 2
- 验证所有构建阶段成功执行
- 验证所有制品正确生成和归档
- 验证推送到头歌成功
- _Requirements: 2.4, 5.5_
- [x] 21. 验证和文档更新
- 测试从本地 Gitea 克隆仓库
- 测试从头歌克隆仓库
- 更新项目 README 文档(如果存在)
- 创建仓库迁移完成报告
- _Requirements: 5.5_

1232
Jenkinsfile vendored

File diff suppressed because it is too large Load Diff

@ -1,109 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.smartlibrary'
compileSdk 34
compileOptions {
coreLibraryDesugaringEnabled false
}
defaultConfig {
applicationId "com.smartlibrary"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
// APK
setProperty("archivesBaseName", "SLMS")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
debug {
debuggable true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// Android 14+
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = '21'
}
buildFeatures {
compose true
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.8'
}
packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation platform('androidx.compose:compose-bom:2024.02.02')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.room:room-ktx:2.6.1'
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.datastore:datastore-preferences:1.0.0'
implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.camera:camera-core:1.3.1'
implementation 'androidx.camera:camera-camera2:1.3.1'
implementation 'androidx.camera:camera-lifecycle:1.3.1'
implementation 'androidx.camera:camera-view:1.3.1'
// implementation project(':backend') // backendSpring BootAndroid
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Android 14+ (API 34+) 权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 存储权限 - Android 13+ 使用分部权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 通知权限 - Android 13+ 需要 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 闹钟权限 -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- 生物识别权限 -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:label="SLMS"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light"
android:requestLegacyExternalStorage="false"
android:hardwareAccelerated="true"
android:largeHeap="true"
android:usesCleartextTraffic="false"
tools:targetApi="34">
<activity
android:name=".android.MainActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Android 14 兼容性配置 -->
<service
android:name=".android.service.LibraryService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<receiver
android:name=".android.receiver.LibraryBroadcastReceiver"
android:exported="false">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

@ -1,233 +0,0 @@
package com.smartlibrary.android;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.smartlibrary.R;
import com.smartlibrary.android.data.DataManager;
import com.smartlibrary.android.model.Book;
import com.smartlibrary.android.model.Loan;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private DataManager dataManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataManager = DataManager.getInstance(this);
initializeMockData();
ScrollView scrollView = new ScrollView(this);
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(40, 40, 40, 40);
TextView titleView = new TextView(this);
titleView.setText("智能图书管理系统 (SLMS)");
titleView.setTextSize(24);
titleView.setTextColor(0xFF1976D2);
titleView.setPadding(0, 0, 0, 30);
layout.addView(titleView);
TextView welcomeView = new TextView(this);
welcomeView.setText("欢迎使用 Android 版本!\n");
welcomeView.setTextSize(16);
layout.addView(welcomeView);
TextView statsView = new TextView(this);
List<Book> books = dataManager.getAllBooks();
List<Loan> loans = dataManager.getLoans();
List<Loan> activeLoans = dataManager.getActiveLoans();
statsView.setText(String.format(
"📊 系统统计\n" +
"━━━━━━━━━━━━━━━━\n" +
"📚 图书总数: %d 本\n" +
"📖 借阅记录: %d 条\n" +
"🔄 当前借阅: %d 本\n" +
"✅ 可借图书: %d 本\n\n",
books.size(),
loans.size(),
activeLoans.size(),
countAvailableBooks(books)
));
statsView.setTextSize(14);
statsView.setLineSpacing(8, 1);
layout.addView(statsView);
TextView booksTitle = new TextView(this);
booksTitle.setText("📚 图书列表");
booksTitle.setTextSize(18);
booksTitle.setTextColor(0xFF1976D2);
booksTitle.setPadding(0, 20, 0, 10);
layout.addView(booksTitle);
for (int i = 0; i < Math.min(books.size(), 10); i++) {
Book book = books.get(i);
TextView bookView = new TextView(this);
bookView.setText(String.format(
"%d. %s\n 作者: %s\n 分类: %s | 状态: %s\n",
i + 1,
book.getTitle(),
book.getAuthor(),
book.getCategory(),
book.isAvailable() ? "✅ 可借" : "❌ 已借出"
));
bookView.setTextSize(13);
bookView.setPadding(10, 10, 10, 10);
bookView.setBackgroundColor(i % 2 == 0 ? 0xFFF5F5F5 : 0xFFFFFFFF);
layout.addView(bookView);
}
if (activeLoans.size() > 0) {
TextView loansTitle = new TextView(this);
loansTitle.setText("\n📖 当前借阅");
loansTitle.setTextSize(18);
loansTitle.setTextColor(0xFF1976D2);
loansTitle.setPadding(0, 20, 0, 10);
layout.addView(loansTitle);
for (int i = 0; i < Math.min(activeLoans.size(), 5); i++) {
Loan loan = activeLoans.get(i);
Book book = dataManager.getBookById(loan.getBookId());
TextView loanView = new TextView(this);
loanView.setText(String.format(
"%d. %s\n 借阅日期: %s\n 应还日期: %s\n",
i + 1,
book != null ? book.getTitle() : "未知图书",
formatDate(loan.getBorrowDate()),
formatDate(loan.getDueDate())
));
loanView.setTextSize(13);
loanView.setPadding(10, 10, 10, 10);
loanView.setBackgroundColor(i % 2 == 0 ? 0xFFFFF3E0 : 0xFFFFFFFF);
layout.addView(loanView);
}
}
TextView footerView = new TextView(this);
footerView.setText("\n✨ 应用已成功启动!\n数据已加载完成。");
footerView.setTextSize(14);
footerView.setTextColor(0xFF4CAF50);
footerView.setPadding(0, 30, 0, 0);
layout.addView(footerView);
scrollView.addView(layout);
setContentView(scrollView);
}
private void initializeMockData() {
if (dataManager.getAllBooks().isEmpty()) {
addMockBooks();
addMockLoans();
}
}
private void addMockBooks() {
Book[] mockBooks = {
new Book("B001", "Java编程思想", "Bruce Eckel", "9787111213826", "编程", "available"),
new Book("B002", "Effective Java", "Joshua Bloch", "9780134685991", "编程", "borrowed"),
new Book("B003", "设计模式", "Erich Gamma", "9787111075752", "软件工程", "available"),
new Book("B004", "代码大全", "Steve McConnell", "9787121022982", "软件工程", "available"),
new Book("B005", "重构", "Martin Fowler", "9787115508645", "软件工程", "borrowed"),
new Book("B006", "算法导论", "Thomas H. Cormen", "9787111407010", "算法", "available"),
new Book("B007", "深入理解计算机系统", "Randal E. Bryant", "9787111544937", "计算机系统", "available"),
new Book("B008", "操作系统概念", "Abraham Silberschatz", "9787111544968", "操作系统", "borrowed"),
new Book("B009", "计算机网络", "Andrew S. Tanenbaum", "9787111453833", "网络", "available"),
new Book("B010", "数据库系统概念", "Abraham Silberschatz", "9787111375296", "数据库", "available"),
new Book("B011", "人工智能", "Stuart Russell", "9787111617143", "AI", "available"),
new Book("B012", "机器学习", "Tom Mitchell", "9787111211396", "AI", "borrowed"),
new Book("B013", "深度学习", "Ian Goodfellow", "9787115461476", "AI", "available"),
new Book("B014", "Python编程", "Eric Matthes", "9787115428028", "编程", "available"),
new Book("B015", "JavaScript高级程序设计", "Nicholas C. Zakas", "9787115275790", "编程", "available")
};
for (Book book : mockBooks) {
dataManager.addBook(book);
}
}
private void addMockLoans() {
java.util.Calendar cal = java.util.Calendar.getInstance();
Loan loan1 = new Loan();
loan1.setId("L001");
loan1.setBookId("B002");
loan1.setUserId("U001");
cal.add(java.util.Calendar.DAY_OF_MONTH, -10);
loan1.setBorrowDate(cal.getTime());
cal.add(java.util.Calendar.DAY_OF_MONTH, 20);
loan1.setDueDate(cal.getTime());
dataManager.addLoan(loan1);
Loan loan2 = new Loan();
loan2.setId("L002");
loan2.setBookId("B005");
loan2.setUserId("U002");
cal = java.util.Calendar.getInstance();
cal.add(java.util.Calendar.DAY_OF_MONTH, -5);
loan2.setBorrowDate(cal.getTime());
cal.add(java.util.Calendar.DAY_OF_MONTH, 25);
loan2.setDueDate(cal.getTime());
dataManager.addLoan(loan2);
Loan loan3 = new Loan();
loan3.setId("L003");
loan3.setBookId("B008");
loan3.setUserId("U003");
cal = java.util.Calendar.getInstance();
cal.add(java.util.Calendar.DAY_OF_MONTH, -15);
loan3.setBorrowDate(cal.getTime());
cal.add(java.util.Calendar.DAY_OF_MONTH, 15);
loan3.setDueDate(cal.getTime());
dataManager.addLoan(loan3);
Loan loan4 = new Loan();
loan4.setId("L004");
loan4.setBookId("B012");
loan4.setUserId("U001");
cal = java.util.Calendar.getInstance();
cal.add(java.util.Calendar.DAY_OF_MONTH, -3);
loan4.setBorrowDate(cal.getTime());
cal.add(java.util.Calendar.DAY_OF_MONTH, 27);
loan4.setDueDate(cal.getTime());
dataManager.addLoan(loan4);
}
private int countAvailableBooks(List<Book> books) {
int count = 0;
for (Book book : books) {
if (book.isAvailable()) {
count++;
}
}
return count;
}
private String formatDate(java.util.Date date) {
if (date == null) return "未设置";
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
@Override
protected void onResume() {
super.onResume();
if (dataManager != null) {
dataManager.syncData();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (dataManager != null) {
dataManager.cleanup();
}
}
}

@ -1,11 +0,0 @@
package com.smartlibrary.android;
import android.app.Activity;
import android.os.Bundle;
public class SimpleActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

@ -1,396 +0,0 @@
package com.smartlibrary.android.data;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.smartlibrary.android.model.Book;
import com.smartlibrary.android.model.Loan;
import com.smartlibrary.android.network.ApiService;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
/**
* -
*
*
*/
public class DataManager extends Observable {
private static final String TAG = "DataManager";
private static final String PREFS_NAME = "smart_library_prefs";
private static final String BOOKS_KEY = "books_key";
private static final String LOANS_KEY = "loans_key";
private static DataManager instance;
private Context context;
private SharedPreferences preferences;
private Gson gson;
private ApiService apiService;
private List<Book> books;
private List<Loan> loans;
private DataManager(Context context) {
this.context = context.getApplicationContext();
this.preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
this.apiService = ApiService.getInstance();
// 从本地存储加载数据
loadDataFromLocal();
Log.d(TAG, "DataManager实例已创建");
}
/**
*
* @param context
* @return DataManager
*/
public static synchronized DataManager getInstance(Context context) {
if (instance == null) {
instance = new DataManager(context);
}
return instance;
}
/**
*
* @return DataManager
*/
public static synchronized DataManager getInstance() {
if (instance == null) {
throw new IllegalStateException("DataManager未初始化请先调用getInstance(Context)");
}
return instance;
}
/**
*
*/
private void loadDataFromLocal() {
// 加载图书数据
String booksJson = preferences.getString(BOOKS_KEY, "");
if (!booksJson.isEmpty()) {
Type bookListType = new TypeToken<List<Book>>() {}.getType();
books = gson.fromJson(booksJson, bookListType);
}
if (books == null) {
books = new ArrayList<>();
}
// 加载借阅数据
String loansJson = preferences.getString(LOANS_KEY, "");
if (!loansJson.isEmpty()) {
Type loanListType = new TypeToken<List<Loan>>() {}.getType();
loans = gson.fromJson(loansJson, loanListType);
}
if (loans == null) {
loans = new ArrayList<>();
}
Log.d(TAG, "从本地存储加载了 " + books.size() + " 本图书和 " + loans.size() + " 条借阅记录");
}
/**
*
*/
private void saveDataToLocal() {
SharedPreferences.Editor editor = preferences.edit();
// 保存图书数据
String booksJson = gson.toJson(books);
editor.putString(BOOKS_KEY, booksJson);
// 保存借阅数据
String loansJson = gson.toJson(loans);
editor.putString(LOANS_KEY, loansJson);
editor.apply();
Log.d(TAG, "数据已保存到本地存储");
}
/**
*
*/
public void syncDataFromServer() {
Log.d(TAG, "开始从服务器同步数据");
// 同步图书数据
apiService.getBooks(new ApiService.ApiCallback<List<Book>>() {
@Override
public void onSuccess(List<Book> result) {
books = result;
saveDataToLocal();
setChanged();
notifyObservers("books_updated");
Log.d(TAG, "图书数据同步成功");
}
@Override
public void onFailure(String errorMessage) {
Log.e(TAG, "图书数据同步失败: " + errorMessage);
}
});
// 同步借阅数据
apiService.getLoans(new ApiService.ApiCallback<List<Loan>>() {
@Override
public void onSuccess(List<Loan> result) {
loans = result;
saveDataToLocal();
setChanged();
notifyObservers("loans_updated");
Log.d(TAG, "借阅数据同步成功");
}
@Override
public void onFailure(String errorMessage) {
Log.e(TAG, "借阅数据同步失败: " + errorMessage);
}
});
}
/**
*
* @return
*/
public List<Book> getBooks() {
return new ArrayList<>(books);
}
/**
* ID
* @param bookId ID
* @return null
*/
public Book getBookById(String bookId) {
for (Book book : books) {
if (book.getId().equals(bookId)) {
return book;
}
}
return null;
}
/**
*
* @param book
*/
public void addBook(Book book) {
books.add(book);
saveDataToLocal();
setChanged();
notifyObservers("book_added");
Log.d(TAG, "添加图书: " + book.getTitle());
}
/**
*
* @param book
*/
public void updateBook(Book book) {
for (int i = 0; i < books.size(); i++) {
if (books.get(i).getId().equals(book.getId())) {
books.set(i, book);
saveDataToLocal();
setChanged();
notifyObservers("book_updated");
Log.d(TAG, "更新图书: " + book.getTitle());
return;
}
}
}
/**
*
* @param bookId ID
*/
public void deleteBook(String bookId) {
for (int i = 0; i < books.size(); i++) {
if (books.get(i).getId().equals(bookId)) {
Book book = books.remove(i);
saveDataToLocal();
setChanged();
notifyObservers("book_deleted");
Log.d(TAG, "删除图书: " + book.getTitle());
return;
}
}
}
/**
*
* @return
*/
public List<Loan> getLoans() {
return new ArrayList<>(loans);
}
/**
* ID
* @param loanId ID
* @return null
*/
public Loan getLoanById(String loanId) {
for (Loan loan : loans) {
if (loan.getId().equals(loanId)) {
return loan;
}
}
return null;
}
/**
*
* @param loan
*/
public void addLoan(Loan loan) {
loans.add(loan);
saveDataToLocal();
setChanged();
notifyObservers("loan_added");
Log.d(TAG, "添加借阅记录: " + loan.getId());
}
/**
*
* @param loan
*/
public void updateLoan(Loan loan) {
for (int i = 0; i < loans.size(); i++) {
if (loans.get(i).getId().equals(loan.getId())) {
loans.set(i, loan);
saveDataToLocal();
setChanged();
notifyObservers("loan_updated");
Log.d(TAG, "更新借阅记录: " + loan.getId());
return;
}
}
}
/**
*
* @param loanId ID
*/
public void deleteLoan(String loanId) {
for (int i = 0; i < loans.size(); i++) {
if (loans.get(i).getId().equals(loanId)) {
Loan loan = loans.remove(i);
saveDataToLocal();
setChanged();
notifyObservers("loan_deleted");
Log.d(TAG, "删除借阅记录: " + loan.getId());
return;
}
}
}
/**
*
*/
public void clearAllData() {
books.clear();
loans.clear();
saveDataToLocal();
setChanged();
notifyObservers("data_cleared");
Log.d(TAG, "所有数据已清空");
}
public void initializeData() {
if (books.isEmpty()) {
Log.d(TAG, "初始化示例数据");
}
}
public void syncData() {
syncDataFromServer();
}
public void cleanup() {
Log.d(TAG, "清理资源");
}
public List<Book> getAllBooks() {
return getBooks();
}
public void borrowBook(String bookId) {
Book book = getBookById(bookId);
if (book != null) {
book.setStatus("borrowed");
updateBook(book);
}
}
public void reserveBook(String bookId) {
Book book = getBookById(bookId);
if (book != null) {
book.setStatus("reserved");
updateBook(book);
}
}
public List<Loan> getActiveLoans() {
List<Loan> activeLoans = new ArrayList<>();
for (Loan loan : loans) {
if (loan.getReturnDate() == null) {
activeLoans.add(loan);
}
}
return activeLoans;
}
public List<Book> getRecentBooks() {
return books.size() > 5 ? books.subList(0, 5) : new ArrayList<>(books);
}
public List<Book> getPopularBooks() {
return books.size() > 5 ? books.subList(0, 5) : new ArrayList<>(books);
}
public List<Loan> getCurrentLoans() {
return getActiveLoans();
}
public List<Loan> getHistoryLoans() {
List<Loan> historyLoans = new ArrayList<>();
for (Loan loan : loans) {
if (loan.getReturnDate() != null) {
historyLoans.add(loan);
}
}
return historyLoans;
}
public void returnBook(String loanId) {
Loan loan = getLoanById(loanId);
if (loan != null) {
loan.setReturnDate(new java.util.Date());
updateLoan(loan);
}
}
public boolean renewBook(String loanId) {
Loan loan = getLoanById(loanId);
if (loan != null && loan.canRenew()) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.setTime(loan.getDueDate());
cal.add(java.util.Calendar.DAY_OF_MONTH, 14);
loan.setDueDate(cal.getTime());
updateLoan(loan);
return true;
}
return false;
}
}

@ -1,28 +0,0 @@
package com.smartlibrary.android.data.model;
public class User {
private String id;
private String username;
private String email;
private String phone;
public User() {}
public User(String id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
}

@ -1,146 +0,0 @@
package com.smartlibrary.android.factory;
import com.smartlibrary.android.model.Book;
import com.smartlibrary.android.model.Loan;
import com.smartlibrary.android.model.User;
import java.util.Date;
import java.util.UUID;
/**
* -
*
*
*/
public class LibraryObjectFactory {
/**
*
* @param title
* @param author
* @param isbn ISBN
* @param category
* @return
*/
public static Book createBook(String title, String author, String isbn, String category) {
Book book = new Book();
book.setId(UUID.randomUUID().toString());
book.setTitle(title);
book.setAuthor(author);
book.setIsbn(isbn);
book.setCategory(category);
book.setStatus("available");
book.setCreatedAt(new Date());
book.setUpdatedAt(new Date());
return book;
}
/**
* ID
* @param id ID
* @param title
* @param author
* @param isbn ISBN
* @param category
* @param status
* @return
*/
public static Book createBook(String id, String title, String author, String isbn, String category, String status) {
Book book = new Book();
book.setId(id);
book.setTitle(title);
book.setAuthor(author);
book.setIsbn(isbn);
book.setCategory(category);
book.setStatus(status);
book.setCreatedAt(new Date());
book.setUpdatedAt(new Date());
return book;
}
/**
*
* @param userId ID
* @param bookId ID
* @param dueDate
* @return
*/
public static Loan createLoan(String userId, String bookId, Date dueDate) {
Loan loan = new Loan();
loan.setId(UUID.randomUUID().toString());
loan.setUserId(userId);
loan.setBookId(bookId);
loan.setLoanDate(new Date());
loan.setDueDate(dueDate);
loan.setStatus("active");
loan.setCreatedAt(new Date());
loan.setUpdatedAt(new Date());
return loan;
}
/**
* ID
* @param id ID
* @param userId ID
* @param bookId ID
* @param loanDate
* @param dueDate
* @param status
* @return
*/
public static Loan createLoan(String id, String userId, String bookId, Date loanDate, Date dueDate, String status) {
Loan loan = new Loan();
loan.setId(id);
loan.setUserId(userId);
loan.setBookId(bookId);
loan.setLoanDate(loanDate);
loan.setDueDate(dueDate);
loan.setStatus(status);
loan.setCreatedAt(new Date());
loan.setUpdatedAt(new Date());
return loan;
}
/**
*
* @param name
* @param email
* @param phone
* @return
*/
public static User createUser(String name, String email, String phone) {
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setName(name);
user.setEmail(email);
user.setPhone(phone);
user.setCreatedAt(new Date());
user.setUpdatedAt(new Date());
return user;
}
/**
* ID
* @param id ID
* @param name
* @param email
* @param phone
* @return
*/
public static User createUser(String id, String name, String email, String phone) {
User user = new User();
user.setId(id);
user.setName(name);
user.setEmail(email);
user.setPhone(phone);
user.setCreatedAt(new Date());
user.setUpdatedAt(new Date());
return user;
}
}

@ -1,116 +0,0 @@
package com.smartlibrary.android.model;
import java.util.Date;
/**
*
*
*/
public class Book {
private String id;
private String title;
private String author;
private String isbn;
private String category;
private String status;
private Date createdAt;
private Date updatedAt;
public Book() {
}
public Book(String id, String title, String author, String isbn, String category, String status) {
this.id = id;
this.title = title;
this.author = author;
this.isbn = isbn;
this.category = category;
this.status = status;
this.createdAt = new Date();
this.updatedAt = new Date();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
public boolean isAvailable() {
return "available".equalsIgnoreCase(status);
}
public String getDescription() {
return "A book by " + author;
}
@Override
public String toString() {
return "Book{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
", category='" + category + '\'' +
", status='" + status + '\'' +
'}';
}
}

@ -1,142 +0,0 @@
package com.smartlibrary.android.model;
import java.util.Date;
/**
*
*
*/
public class Loan {
private String id;
private String userId;
private String bookId;
private Date loanDate;
private Date dueDate;
private Date returnDate;
private String status;
private Date createdAt;
private Date updatedAt;
public Loan() {
}
public Loan(String id, String userId, String bookId, Date loanDate, Date dueDate, String status) {
this.id = id;
this.userId = userId;
this.bookId = bookId;
this.loanDate = loanDate;
this.dueDate = dueDate;
this.status = status;
this.createdAt = new Date();
this.updatedAt = new Date();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getBookId() {
return bookId;
}
public void setBookId(String bookId) {
this.bookId = bookId;
}
public Date getLoanDate() {
return loanDate;
}
public void setLoanDate(Date loanDate) {
this.loanDate = loanDate;
}
public Date getBorrowDate() {
return loanDate;
}
public void setBorrowDate(Date borrowDate) {
this.loanDate = borrowDate;
}
public Date getDueDate() {
return dueDate;
}
public void setDueDate(Date dueDate) {
this.dueDate = dueDate;
}
public Date getReturnDate() {
return returnDate;
}
public void setReturnDate(Date returnDate) {
this.returnDate = returnDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
public boolean isOverdue() {
if (returnDate != null) return false;
return dueDate != null && new Date().after(dueDate);
}
@Override
public String toString() {
return "Loan{" +
"id='" + id + '\'' +
", userId='" + userId + '\'' +
", bookId='" + bookId + '\'' +
", loanDate=" + loanDate +
", dueDate=" + dueDate +
", status='" + status + '\'' +
'}';
}
public String getBookTitle() {
return "Book Title";
}
public String getBookAuthor() {
return "Book Author";
}
public boolean canRenew() {
return returnDate == null && !isOverdue();
}
}

@ -1,86 +0,0 @@
package com.smartlibrary.android.model;
import java.util.Date;
/**
*
*
*/
public class User {
private String id;
private String name;
private String email;
private String phone;
private Date createdAt;
private Date updatedAt;
public User() {
}
public User(String id, String name, String email, String phone) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.createdAt = new Date();
this.updatedAt = new Date();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", email='" + email + '\'' +
", phone='" + phone + '\'' +
'}';
}
}

@ -1,343 +0,0 @@
package com.smartlibrary.android.network;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.smartlibrary.android.model.Book;
import com.smartlibrary.android.model.Loan;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* API -
*
* CRUD
*/
public class ApiService {
private static final String TAG = "ApiService";
private static final String BASE_URL = "http://10.0.2.2:8080/SLMS/api/"; // 模拟器访问本机地址
private static ApiService instance;
private Retrofit retrofit;
private LibraryApi libraryApi;
private Gson gson;
private ApiService() {
// 配置Gson
gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.create();
// 配置OkHttpClient
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new HttpLoggingInterceptor())
.build();
// 配置Retrofit
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
libraryApi = retrofit.create(LibraryApi.class);
Log.d(TAG, "ApiService实例已创建");
}
/**
*
* @return ApiService
*/
public static synchronized ApiService getInstance() {
if (instance == null) {
instance = new ApiService();
}
return instance;
}
/**
*
* @param callback
*/
public void getBooks(ApiCallback<List<Book>> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.getBooks()
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(1000);
// 模拟返回数据
List<Book> books = getMockBooks();
// 在主线程回调
if (callback != null) {
callback.onSuccess(books);
}
} catch (Exception e) {
Log.e(TAG, "获取图书失败", e);
if (callback != null) {
callback.onFailure("获取图书失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @param callback
*/
public void getLoans(ApiCallback<List<Loan>> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.getLoans()
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(1000);
// 模拟返回数据
List<Loan> loans = getMockLoans();
// 在主线程回调
if (callback != null) {
callback.onSuccess(loans);
}
} catch (Exception e) {
Log.e(TAG, "获取借阅记录失败", e);
if (callback != null) {
callback.onFailure("获取借阅记录失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @param book
* @param callback
*/
public void addBook(Book book, ApiCallback<Book> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.addBook(book)
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(500);
// 在主线程回调
if (callback != null) {
callback.onSuccess(book);
}
} catch (Exception e) {
Log.e(TAG, "添加图书失败", e);
if (callback != null) {
callback.onFailure("添加图书失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @param book
* @param callback
*/
public void updateBook(Book book, ApiCallback<Book> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.updateBook(book)
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(500);
// 在主线程回调
if (callback != null) {
callback.onSuccess(book);
}
} catch (Exception e) {
Log.e(TAG, "更新图书失败", e);
if (callback != null) {
callback.onFailure("更新图书失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @param bookId ID
* @param callback
*/
public void deleteBook(String bookId, ApiCallback<Boolean> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.deleteBook(bookId)
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(500);
// 在主线程回调
if (callback != null) {
callback.onSuccess(true);
}
} catch (Exception e) {
Log.e(TAG, "删除图书失败", e);
if (callback != null) {
callback.onFailure("删除图书失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @param loan
* @param callback
*/
public void addLoan(Loan loan, ApiCallback<Loan> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.addLoan(loan)
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(500);
// 在主线程回调
if (callback != null) {
callback.onSuccess(loan);
}
} catch (Exception e) {
Log.e(TAG, "添加借阅记录失败", e);
if (callback != null) {
callback.onFailure("添加借阅记录失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @param loan
* @param callback
*/
public void updateLoan(Loan loan, ApiCallback<Loan> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.updateLoan(loan)
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(500);
// 在主线程回调
if (callback != null) {
callback.onSuccess(loan);
}
} catch (Exception e) {
Log.e(TAG, "更新借阅记录失败", e);
if (callback != null) {
callback.onFailure("更新借阅记录失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @param loanId ID
* @param callback
*/
public void deleteLoan(String loanId, ApiCallback<Boolean> callback) {
// 模拟网络请求实际项目中应该调用libraryApi.deleteLoan(loanId)
new Thread(() -> {
try {
// 模拟网络延迟
Thread.sleep(500);
// 在主线程回调
if (callback != null) {
callback.onSuccess(true);
}
} catch (Exception e) {
Log.e(TAG, "删除借阅记录失败", e);
if (callback != null) {
callback.onFailure("删除借阅记录失败: " + e.getMessage());
}
}
}).start();
}
/**
*
* @return
*/
private List<Book> getMockBooks() {
List<Book> books = new ArrayList<>();
// 添加一些模拟图书数据
books.add(new Book("1", "Java编程思想", "Bruce Eckel", "9787111213826", "计算机", "available"));
books.add(new Book("2", "设计模式", "Erich Gamma", "9787111075756", "计算机", "available"));
books.add(new Book("3", "重构", "Martin Fowler", "9787115508645", "计算机", "borrowed"));
books.add(new Book("4", "代码整洁之道", "Robert C. Martin", "9787115216878", "计算机", "available"));
books.add(new Book("5", "算法导论", "Thomas H. Cormen", "9787111407928", "计算机", "borrowed"));
return books;
}
/**
*
* @return
*/
private List<Loan> getMockLoans() {
List<Loan> loans = new ArrayList<>();
// 添加一些模拟借阅数据
java.util.Calendar calendar = java.util.Calendar.getInstance();
// 借阅记录1
calendar.add(java.util.Calendar.DAY_OF_MONTH, -7);
java.util.Date loanDate1 = calendar.getTime();
calendar.add(java.util.Calendar.DAY_OF_MONTH, 21);
java.util.Date dueDate1 = calendar.getTime();
Loan loan1 = new Loan("1", "user1", "3", loanDate1, dueDate1, "active");
loans.add(loan1);
// 借阅记录2
calendar = java.util.Calendar.getInstance();
calendar.add(java.util.Calendar.DAY_OF_MONTH, -14);
java.util.Date loanDate2 = calendar.getTime();
calendar.add(java.util.Calendar.DAY_OF_MONTH, 14);
java.util.Date dueDate2 = calendar.getTime();
Loan loan2 = new Loan("2", "user2", "5", loanDate2, dueDate2, "active");
loans.add(loan2);
return loans;
}
/**
* API
* @param <T>
*/
public interface ApiCallback<T> {
/**
*
* @param result
*/
void onSuccess(T result);
/**
*
* @param errorMessage
*/
void onFailure(String errorMessage);
}
}

@ -1,48 +0,0 @@
package com.smartlibrary.android.network;
import android.util.Log;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* HTTP
*
*/
public class HttpLoggingInterceptor implements Interceptor {
private static final String TAG = "HttpLogging";
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startTime = System.currentTimeMillis();
Log.d(TAG, "发送请求: " + request.method() + " " + request.url());
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
Log.e(TAG, "请求失败: " + e.getMessage());
throw e;
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
MediaType contentType = response.body().contentType();
String responseBody = response.body().string();
Log.d(TAG, "收到响应: " + response.code() + " (" + duration + "ms)");
Log.d(TAG, "响应内容: " + responseBody);
// 重新创建响应因为body.string()只能调用一次
ResponseBody newResponseBody = ResponseBody.create(contentType, responseBody);
return response.newBuilder().body(newResponseBody).build();
}
}

@ -1,101 +0,0 @@
package com.smartlibrary.android.network;
import com.smartlibrary.android.model.Book;
import com.smartlibrary.android.model.Loan;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
/**
* API
* REST API
*/
public interface LibraryApi {
/**
*
* @return
*/
@GET("books")
Call<List<Book>> getBooks();
/**
* ID
* @param id ID
* @return
*/
@GET("books/{id}")
Call<Book> getBook(@Path("id") String id);
/**
*
* @param book
* @return
*/
@POST("books")
Call<Book> addBook(@Body Book book);
/**
*
* @param id ID
* @param book
* @return
*/
@PUT("books/{id}")
Call<Book> updateBook(@Path("id") String id, @Body Book book);
/**
*
* @param id ID
* @return
*/
@DELETE("books/{id}")
Call<Void> deleteBook(@Path("id") String id);
/**
*
* @return
*/
@GET("loans")
Call<List<Loan>> getLoans();
/**
* ID
* @param id ID
* @return
*/
@GET("loans/{id}")
Call<Loan> getLoan(@Path("id") String id);
/**
*
* @param loan
* @return
*/
@POST("loans")
Call<Loan> addLoan(@Body Loan loan);
/**
*
* @param id ID
* @param loan
* @return
*/
@PUT("loans/{id}")
Call<Loan> updateLoan(@Path("id") String id, @Body Loan loan);
/**
*
* @param id ID
* @return
*/
@DELETE("loans/{id}")
Call<Void> deleteLoan(@Path("id") String id);
}

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_on_primary">
<path
android:fillColor="@android:color/white"
android:pathData="M21,5c-1.11,-0.35 -2.33,-0.5 -3.5,-0.5 -1.95,0 -4.05,0.4 -5.5,1.5 -1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5 1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5V6c-0.6,-0.45 -1.25,-0.75 -2,-1zM21,18.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5V8c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.2,0 2.4,0.15 3.5,0.5v11.5z"/>
</vector>

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/primary_light" />
</shape>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2l3.09,6.26L22,9.27l-5,4.87 1.18,6.88L12,17.77l-6.18,3.25L7,14.14 2,9.27l6.91,-1.01L12,2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M7,10l5,5 5,-5z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M7,14l5,-5 5,5z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17H7v-2h7v2zM17,13H7v-2h10v2zM17,9H7V7h10v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17H7v-2h7v2zM17,13H7v-2h10v2zM17,9H7L7,7h10v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM10,17l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M21,5c-1.11,-0.35 -2.33,-0.5 -3.5,-0.5 -1.95,0 -4.05,0.4 -5.5,1.5 -1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5 1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5V6c-0.6,-0.45 -1.25,-0.75 -2,-1zM21,18.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5V8c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.2,0 2.4,0.15 3.5,0.5v11.5z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M18,2H6c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM6,4h5v8l-2.5,-1.5L6,12V4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2l3.09,6.26L22,9.27l-5,4.87 1.18,6.88L12,17.77l-6.18,3.25L7,14.14 2,9.27l6.91,-1.01L12,2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_on_primary">
<path
android:fillColor="@android:color/white"
android:pathData="M21,5c-1.11,-0.35 -2.33,-0.5 -3.5,-0.5 -1.95,0 -4.05,0.4 -5.5,1.5 -1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5 1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5V6c-0.6,-0.45 -1.25,-0.75 -2,-1zM21,18.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5V8c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.2,0 2.4,0.15 3.5,0.5v11.5z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/darker_gray"
android:pathData="M18,2H6c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM6,4h5v8l-2.5,-1.5L6,12V4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M9,11H7v2h2v-2zM13,11h-2v2h2v-2zM17,11h-2v2h2v-2zM19,4h-1L18,2h-2v2L8,4L8,2L6,2v2L5,4c-1.11,0 -1.99,0.9 -1.99,2L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM19,20L5,20L5,9h14v11z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M20,6h-2.18c0.11,-0.31 0.18,-0.65 0.18,-1 0,-1.66 -1.34,-3 -3,-3 -1.05,0 -1.96,0.54 -2.5,1.35l-0.5,0.67 -0.5,-0.68C10.96,2.54 10.05,2 9,2 7.34,2 6,3.34 6,5c0,0.35 0.07,0.69 0.18,1L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM15,5c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM9,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM10,17L5,12l1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM19,19H5V5h14v14z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M18,2h-3.17C14.42,0.84 13.3,0 12,0S9.58,0.84 9.17,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM13,18L11,18v-2h2v2zM13,14L11,14v-2h2v2zM13,10L11,10L11,8h2v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M14,2H6c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,7h2v6h-2zM11,15h2v2h-2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6h-2v6h2V6z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M9,11H7v2h2v-2zM13,11h-2v2h2v-2zM17,11h-2v2h2v-2zM19,4h-1V2h-2v2H8V2H6v2H5c-1.11,0 -1.99,0.9 -1.99,2L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2zM19,20H5V9h14v11z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M9,11H7v2h2v-2zM13,11h-2v2h2v-2zM17,11h-2v2h2v-2zM19,4h-1V2h-2v2H8V2H6v2H5c-1.11,0 -1.99,0.9 -1.99,2L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2zM19,20H5V9h14v11z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-1V1h-2v2H8V1H6v2H5c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM19,19H5V8h14v11zM7,10h5v5H7z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zm-4.4,15.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18V6H3zm3,7h12v-2H6v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M10,4H4c-1.11,0 -2,0.89 -2,2v12c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2h-8l-2,-2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM5.49,9.99C5.49,7.01 7.51,5 10.49,5c1.48,0 2.75,0.54 3.76,1.44l-1.62,1.56C12.23,7.59 11.44,7.29 10.49,7.29c-1.48,0 -2.68,1.21 -2.68,2.7s1.2,2.7 2.68,2.7c0.86,0 1.54,-0.33 2.02,-0.78h-2.16v-2.05h4.61c0.08,0.4 0.12,0.82 0.12,1.26 0,3.46 -2.35,5.88 -5.59,5.88 -3.31,0 -6,-2.69 -6,-6.01z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_on_primary">
<path
android:fillColor="@android:color/white"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,7h2v6h-2zM11,15h2v2h-2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2V7h2v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M14,2H6c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.94,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
</vector>

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_on_primary">
<path
android:fillColor="@android:color/white"
android:pathData="M18,2H6c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM6,4h5v8l-2.5,-1.5L6,12V4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h18v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h18v-2L3,11v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M21,5c-1.11,-0.35 -2.33,-0.5 -3.5,-0.5 -1.95,0 -4.05,0.4 -5.5,1.5 -1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5 1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5L23,6c-0.6,-0.45 -1.25,-0.75 -2,-1zM21,18.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5L12,8c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.2,0 2.4,0.15 3.5,0.5v11.5z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h18v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h18v-2L3,11v2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,17H9V7h2V17zM15,17h-2V7h2V17z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5V19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45V19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1H5.9V17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_on_primary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/text_secondary">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.94,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save