Compare commits

..

19 Commits

Author SHA1 Message Date
4b196acfce <powerbell>APK 15.15.8 release Publish. 2026-02-04 11:07:22 +08:00
cf857c1359 尝试叫豆包修复每天10半左右会无端端调用TTS服务的问题。 2026-02-04 11:04:13 +08:00
81b758ddc5 <powerbell>APK 15.15.7 release Publish. 2026-01-24 20:41:05 +08:00
64a53058cc 增加对签名证书修改后的证书识别能力。 2026-01-24 20:38:52 +08:00
aae17b6cd2 <powerbell>APK 15.15.6 release Publish. 2026-01-24 12:44:08 +08:00
d6637e1c17 更新基础类库,新类库应用验证模块有改进。 2026-01-24 12:41:51 +08:00
6d425cab5c <powerbell>APK 15.15.5 release Publish. 2026-01-23 03:18:09 +08:00
ab566f76ff 添加应用签名联网校验模块。 2026-01-23 03:15:06 +08:00
cd04458c62 <powerbell>APK 15.15.4 release Publish. 2026-01-20 21:21:19 +08:00
6b46180da7 应用指纹校验对话框显示优化。 2026-01-20 21:19:53 +08:00
af8b57b735 <powerbell>APK 15.15.3 release Publish. 2026-01-20 21:00:50 +08:00
21c02db577 更新基础类库,添加应用签名校验。 2026-01-20 20:59:01 +08:00
49a35829d2 <powerbell>APK 15.15.2 release Publish. 2026-01-19 20:40:18 +08:00
8229ab099a 更新应用设置窗口的TTS服务选项卡的处理逻辑。 2026-01-19 20:38:46 +08:00
45400314af <powerbell>APK 15.15.1 release Publish. 2026-01-19 20:04:16 +08:00
f53113d0df TTS贴心服务加入当前电量语音提醒功能。 2026-01-19 20:01:28 +08:00
53fb1ac835 <powerbell>APK 15.15.0 release Publish. 2026-01-13 11:47:03 +08:00
be9855b29d 更新基础类库与应用介绍页。 2026-01-13 11:44:12 +08:00
75e047f1d5 添加 https://gitea.winboll.cc/Studio/WinBoLL_Bck20260112_122031_590.git PowerBell 分支源码。 2026-01-13 04:09:05 +08:00
252 changed files with 21352 additions and 6857 deletions

236
README.md
View File

@@ -1,103 +1,175 @@
# WinBoLL 源生态计划项目说明书
## OriginMaster
【OriginMaster】WinBoLL 源生态计划。正如话,我需要一个 Point, 去撬动成个地球。
WinBoLL系列项目各个项目源码合并推送方向
WinBoLL=>APPBase=>OriginMaster
WinBoLL=>AES=>OriginMaster
WinBoLL=>PowerBell=>OriginMaster
WinBoLL=>Positions=>OriginMaster
仓库规划概述:
☆ WinBoLL,APPBase,AES,PowerBell与Positions都是开发库。
☆ OriginMaster 是分支汇总存档库。
☆ OriginMaster 不适宜作为开发库克隆使用,不是应用开发基础库,而是汇总资料库。
☆ WinBoLL 库可以作为应用开发的基础继承模板。
## 一、项目概述
########
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/winboll.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码<https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码<https://github.com/ZhanGSKen/AES.git> ☁ ☁ ☁ ☁
## WinBoLL 提问
同样是 /sdcard 目录,在开发 Android 应用时,
能否实现手机编译与电脑编译的源码同步。
☁因而 WinBoLL 项目组诞生了。
### 1. 核心定位
WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与服务器端的 Android 项目的开发源码生态。实现手机与服务器的源码的联合开发。
## WinBoLL 项目组研发计划
致力于把 WinBoLL-APP 应用在手机端 Android 项目开发。
也在探索 https://gitea.winboll.cc/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP APK 分发。
更想进阶 https://github.com/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP Beta APK 分发。
### 2. 仓库架构
#### **仓库类型:功能说明**
☆ 基础项目分支 WinBoLL手机端安卓应用开发基础模板。
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
## WinBoLL-APP 汗下...
#### ☁应用何置如此呢。且观用户云云。
### 3. 源码合并管理推送路线图
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
#### ☁ 正当下 ☁ ###
#### ☁ 且容傻家叙说 ☁ WinBoLL-APP 应用场景
### ☁ WinBoLL 设备资源概述
#### ☁ 1. Raid Disk.
概述:这是一个矩阵存储类设备。
优点:该设备具有数据容错存储功能,
数据存储具有特长持久性。
缺点:设备使用能源消耗比较高,
设备存取速度一般。
#### ☁ 2. Data Disk.
概述:这是一个普通硬盘存储设备
优点:该设备独立于操作系统,
数据持久性一般,
存取能源消耗小于 Raid Disk。
缺点:数据存储速度一般,存储能源消耗一般。
★ WinBoLL → APPBase → OriginMaster
★ WinBoLL → AES → OriginMaster
★ WinBoLL → PowerBell → OriginMaster
★ WinBoLL → Positions → OriginMaster
#### ☁ 3. SSD Disk.
概述:这是一个 SSD 硬盘存储设备。
优点:存取速度快于 Data Disk 与 Raid Disk
存取能源消耗小于 Data Disk 与 Raid Disk。
缺点:数据持久性一般,
设备位于操作系统内部文件系统。
数据持久性与操作系统挂钩。
#### ☁ 4. WinBoLL 用户资源概述。
1> /home/<用户名> 位于 WinBoLL 操作系统目录下。
2> /rdisk/<用户名> 挂载用户 Raid Disk.
3> /data/<用户名> 挂载用户 Data Disk.
4> /sdcard/<用户名> 挂载用户 SSD Disk.
## 二、WinBoLL 项目核心信息
#### ☁ 5. WinBoLL-APP 用户资源概述。
1> /sdcard 挂载用户手机 SD 存储/storage/emulated/0
### 1. 项目简介
☆ WinBoLL 项目是为手机端开发Android 项目的需求而设计的项目。
### ☁ 稍稍歇 ☁ ###
### ☁ 急急停 ☁ WinBoLL 应用前置条件
☁ WinBoLL 主机建立 1Panel MySQL 应用。
☁ WinBoLL 主机建立 1Panel Gitea 应用。
☁ WinBoLL 主机设置 WinBoLL 应用为非登录状态。
☁ WinBoLL 主机建立 WinBoLL 账户与 WinBoLL 用户组。
☁ WinBoLL 账户 User ID 为: J。
☁ WinBoLL 用户组 Group ID 为: Studio。
☁ WinBoLL 主机 WinBoLL 1Panel Gitea 建立 WinBoLL 工作组。
☁ WinBoLL 主机 WinBoLL 1Panel Gitea 用户项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/<用户名>/APKs/
☁ WinBoLL 项目配置文件示例为 "<WinBoLL 项目根目录>/.winboll/winboll.properties-demo"(WinBoLL 项目已设置)
☁ WinBoLL 项目配置文件为 "<WinBoLL 项目根目录>/.winboll/winboll.properties"
☁ WinBoLL 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置)
☁ Gradle 项目配置文件示例为 "<WinBoLL 项目根目录>/.winboll/local.properties-demo"(WinBoLL 项目已设置)
☁ Gradle 项目配置文件为 "<WinBoLL 项目根目录>/local.properties"(WinBoLL 项目已设置)
☁ Gradle 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置)
### 2. 官方资源
#### ☆ 官方网站**https://www.winboll.cc/
#### ☆ 源码地址:
★ Giteahttps://gitea.winboll.cc/Studio/WinBoLL.git
★ GitHubhttps://github.com/ZhanGSKen/WinBoLL.git
★ 码云https://gitee.com/zhangsken/winboll.git
### ☁ 登高处 ☁ WinBoLL 应用需求规划
☁ WinBoLL 主机建立 WinBoLL 客户端用户数据库为 MySQL winbollclient 数据库。
☁ WinBoLL 主机设置 WinBoLL 客户端用户信息存储在 winbollclient 数据库中。
☁ MySQL winbollclient 数据库中
WinBoLL 客户端用户信息设定为:
<用户名, 验证密码, 验证邮箱, 验证手机, 唯一存储令牌Token, 备用验证邮箱>
☁ WinBoLL 项目源码仓库托管在 WinBoLL 1Panel Gitea 目录 /opt/1panel/apps/gitea/gitea/data/git/repositories/studio/app.git中。
☁ WinBoLL 主机提供 WinBoLL 1Panel Gitea 应用的 WinBoLL 项目源码仓库存取功能。Gitea 应用已提供)
☁ WinBoLL 主机提供 WinBoLL Gitea 项目仓库存档功能。Gitea 应用已提供)
☁ 提供 WinBoLL 客户端用户登录功能。Gitea 应用已提供)
## 三、应用编译环境检查问题
### 核心判断条件:
WinBoLL 项目以文件夹 `"/sdcard/WinBoLLStudio/APKs"` 是否存在为判断环境编译输出条件因为编译输出的APK文件需要一个可供保存的环境。
### ☁ 看远方 ☁ ###
### ☁ 心忧虑 ☁ WinBoLL-APP 应用前置需求
☁ WinBoLL-APP WinBoLL 项目根目录设定为手机的 /sdcard/WinBoLLStudio/Sources 目录。(需要用户手动建立文件夹)
☁ WinBoLL-APP 具有手机 /sdcard/WinBoLL 目录的存储权限。(需要手机操作系统授权)
☁ WinBoLL-APP WinBoLL 项目仓库源码存储路径为 /sdcard/WinBoLLStudio/Sources/APP.git需要用户手动建立文件夹
☁ WinBoLL-APP 项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/APKs/
☁ WinBoLL-APP 应用签名验证可定制化。WinBoLL 项目已提供)
☁ WinBoLL-APP 与系列衍生 APP 应用共享 cc.winboll.studio 命名空间资源。WinBoLL 项目已提供)
☁ WinBoLL-APP 用户客户端信息存储在命名空间为 WinBoLL APP MySQLLite 应用的 winbollappclient 数据库中。
☁ WinBoLL-APP MySQLLite 应用的 winbollappclient 数据库中,
WinBoLL 用户客户端信息设定为:
<用户名, 唯一存储令牌Token>
☆ 文件夹"/sdcard/WinBoLLStudio/APKs" 目录条件设置方法:
***Linux 服务器端方面***:建立 `/sdcard/WinBoLLStudio/APKs` 目录即可。
***手机开发端方面***:建立 `"/sdcard/WinBoLLStudio/APKs"` 目录(即 `"/storage/emulated/0/WinBoLLStudio/APKs"` 目录) 即可
### ☁ 云游四方 ☁ ###
### ☁ 呔! ☁ WinBoLL-APP 应用需求规划
☁ 如要使用 WinBoLL Android 项目的 Gradle 编译功能,则需要设置以下两个文件夹。
☁ 1. 则需要建立数据存储目录 /sdcard/WinBoLLStudio/APKs。
WinBoLL 项目源码编译出来的安装包会拷贝一份到 /sdcard/WinBoLLStudio/APKs 目录下。
☁ 2. 则需要建立数据存储目录 /sdcard/AppProjects。
WinBoLL 项目源码编译出来的安装包会拷贝一份并命名 "app.apk" 的安装文件为到 /sdcard/AppProjects 目录下。
## 四、前置条件
### 1. WinBoLL APP 开发环境配置介绍
#### WinBoLL APK 编译输出内容包括:
☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件
☆ "/sdcard/AppProjects/app.apk"文件。
#### WinBoLL APK 源码命名空间规范
☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。
### ☁ 吁! ☁ WinBoLL-APP 共享计划前景
WinBoLL-APP 将会实现 https://winboll.cc/api 访问功能。
☁ WinBoLL-APP 将会实现手机端 Android 应用的开发与管理功能
## 五、核心需求规划
## ☁ WinBoLL ☁ WinBoLL 主机忧虑
☁ WinBoLL 将会提供 gitea.winboll.cc 域名用户注册登录功能。
☁ WinBoLL 将会提供 WinBoLL-APP 及其衍生应用的 Gitea 仓库管理服务。
☁ WinBoLL 将会提供 winboll.cc 域名 WinBoLL 项目组注册登录功能。
### 1. WinBoLL 应用安全验证需求
#### ☆ 支持访问 https://console.winboll.cc/ 服务器以校验应用包签名与版本。
# 本项目要实际运用需要注意以下几个步骤:
# 在项目根目录下:
## ★. 项目模块编译环境设置(必须)settings.gradle-demo 要复制为 settings.gradle并取消相应项目模块的注释。
## ★. 项目模块编译环境设置(必须) 在根目录拷贝 gradle.properties-androidx-demo 或者 gradle.properties-android-demo 文件为 gradle.properties。
## ★. 项目 Android SDK 编译环境设置(可选)local.properties-demo 要复制为 local.properties并按需要设置 Android SDK 目录。
## ★. 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置。
## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。
## ☆. 类库型模块编译环境设置(可选)winboll.properties-demo 要复制为 winboll.properties并按需要设置 WinBoLL Maven 库登录用户信息, 和 APK 文件额外输出路径。
### 2. 手机端源码开发管理需求
#### ☆ 支持切换不同 WinBoLL 分支,以开发不同安卓应用。
## 六、编译与使用指南
# ☆类库型项目编译方法
## 先编译类库对应的模块测试项目
### 修改模块测试项目的 build.properties 文件
设置属性 libraryProject=<类库项目模块文件夹名称>
### 再编译测试项目
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
### 最后编译类库项目
$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块文件夹名称>
#### 类库模块编译命令执行后,编译器会发布到 WinBoLL Nexus Maven 库Maven 库地址可以参阅根项目目录配置 build.gradle 文件。
# ☆应用型项目编译方法
## 直接调用以下命令编译应用型项目
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
#### 应用模块编译命令执行后,编译器会复制一份 APK 到
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
### 1. 项目初始化(必须)
#### 1. 复制 `settings.gradle-demo` 为 `settings.gradle`。编辑 `settings.gradle` 文件内容,取消对应项目模块注释。
#### 2. 复制 `gradle.properties-androidx-demo` (Android X 项目) 或 `gradle.properties-android-demo` (基本 Android 项目) 为 `gradle.properties`。
#### 3. 复制(可选)`local.properties-demo` 为 `local.properties`,编辑 `local.properties` 文件内容,配置 Android SDK 目录。
#### 4. **签名设置**
☆ **调试编译秘钥制作**:使用 Termux 应用终端cd 进入 GenKeyStore 目录,运行 `bash gen_debug_keystore.sh` 脚本即可生成应用调试秘钥。
☆ **应用秘钥配置方法**:拷贝调试编译秘钥制作生成的 `appkey.jks` 与 `appkey.keystore` 文件到项目根目录即可。
## ☆应用调试编译方法
使用以下命令编译调试:
## 七、应用编译命令介绍
### Beta调试使用
$ bash gradlew assembleBetaDebug
### 1类库型模块配置要点
#### 1. **优先修改配置文件**:优先修改应用测试项目(目录为 `"<WinBoLl根目录>/<类库测试应用>/"`)内 `build.properties` 文件,设置对应的类库项目名称:`libraryProject=<类库项目模块名>`。
#### 2. **编译优先启动步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <类库测试项目模块名>` 命令。运行后可生成测试项目与类库项目的编译参数文件 `build.properties`。生成的 `build.properties` 文件有两份,一份在测试项目模块的文件夹内,一份在类库项目本身的模块文件夹内。
#### 3. **最后类库编译发布步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名>` 命令。运行后可发布至 WinBoLL Nexus Maven 库、本地 maven 目录或者是通用默认的 Gradle Maven 库。
### Stage调试使用
$ bash gradlew assembleStageDebug
### 2单一应用型模块与类库测试型模块配置要点
#### ☆ APK 编译方法:
使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>`。
#### ☆ 运行后的 APK 输出路径:
★ 默认路径 (`$ bash gradlew assembleBetaDebug` 任务)APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/debug/` 目录。
★ 默认路径 (`$ bash assembleStageRelease` 任务)APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/` 目录。
★ 额外输出路径:(假设 `winboll.properties` 文件已配置 `ExtraAPKOutputPath` 属性) 输出至 `ExtraAPKOutputPath` 属性配置的目录下。
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
### 3手机端应用调试命令介绍
#### ☆ Beta 渠道调试命令
$bash gradlew assembleBetaDebug
#### ☆ Stage 渠道调试命令
$bash gradlew assembleStageDebug
### 4服务器端开发命令介绍
##### ☆ Stage 渠道应用发布命令为:
"<WinBoLl根目录>/settings.gradle"文件需要配置编译模块开启参数,拷贝 settings.gradle-demo 为 settings.gradle 文件取消对应的分支配置部分即可。)
$bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名> 
或者是
$bash gradlew assembleStageRelease
## 八、WinBoLL 应用 APK 版本号命名规则
### ☆ Stage 渠道:
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号> 示例 APPBase_15.7.0 
### ☆ Beta 渠道:
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> 示例 APPBase_15.9.6-beta8_5413 
# 应用版本号命名方式
## statge 渠道
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
APPBase_15.7.0
## beta 渠道
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
APPBase_15.9.6-beta8_5413

View File

@@ -1,15 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/google/' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
maven { url "https://clojars.org/repo/" }
maven { url "https://jitpack.io" }
mavenCentral()
google()
mavenLocal {
// 设置本地Maven仓库路径
url 'file:///sdcard/.m2/repository/'
@@ -20,6 +11,19 @@ buildscript {
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
// "WinBoLL Snapshot"
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/google/' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
maven { url "https://clojars.org/repo/" }
maven { url "https://jitpack.io" }
mavenCentral()
google()
//println "mavenLocal : ==========="
//println mavenLocal().url
//println "mavenLocal : ==========="
//mavenLocal()
}
dependencies {
// 适配MIUI12
@@ -31,15 +35,6 @@ buildscript {
allprojects {
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/google/' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
maven { url "https://clojars.org/repo/" }
maven { url "https://jitpack.io" }
mavenCentral()
google()
mavenLocal {
// 设置本地Maven仓库路径
url 'file:///sdcard/.m2/repository/'
@@ -50,6 +45,19 @@ allprojects {
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
// "WinBoLL Snapshot"
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/google/' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
maven { url "https://clojars.org/repo/" }
maven { url "https://jitpack.io" }
mavenCentral()
google()
//println "mavenLocal : ==========="
//println mavenLocal().url
//println "mavenLocal : ==========="
//mavenLocal()
}
ext {
// 定义全局变量,常用于版本管理

View File

@@ -1,34 +0,0 @@
# Gallery
#### 介绍
云宝相册应用
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh gallery
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档

View File

@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Fri May 01 10:29:10 HKT 2026
stageCount=15
libraryProject=
baseVersion=15.0
publishVersion=15.0.14
buildCount=0
baseBetaVersion=15.0.15

View File

@@ -1,12 +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" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
<string name="app_name">Gallery+</string>
</resources>

View File

@@ -1,66 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.gallery">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:resizeableActivity="true"
android:name=".GlobalWinBoLLApplication"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/settings_title"/>
<activity
android:name=".AlbumActivity"
android:label="@string/app_name"/>
<activity
android:name=".ImageViewerActivity"
android:label="@string/app_name"/>
<activity
android:name=".TrashActivity"
android:label="@string/trash"/>
<activity
android:name=".CropActivity"
android:label="调整封面"/>
<activity
android:name=".AboutActivity"
android:label="@string/about"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
</application>
</manifest>

View File

@@ -1,51 +0,0 @@
package cc.winboll.studio.gallery;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
public class AboutActivity extends AppCompatActivity {
public static final String TAG = "AboutActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "gallery";
APPInfo appInfo = new APPInfo();
appInfo.setAppName("Gallery");
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("WinBoLL");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Gallery");
appInfo.setAppAPKName("Gallery");
appInfo.setAppAPKFolderName("Gallery");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}

View File

@@ -1,25 +0,0 @@
package cc.winboll.studio.gallery;
import android.net.Uri;
import cc.winboll.studio.libappbase.LogUtils;
public class Album {
public static final String TAG = "Album";
private String name;
private String path;
private Uri coverUri;
private int imageCount;
public Album(String name, String path, Uri coverUri, int imageCount) {
LogUtils.d(TAG, "Album created: " + name);
this.name = name;
this.path = path;
this.coverUri = coverUri;
this.imageCount = imageCount;
}
public String getName() { return name; }
public String getPath() { return path; }
public Uri getCoverUri() { return coverUri; }
public int getImageCount() { return imageCount; }
}

View File

@@ -1,251 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.gallery.ImageAdapter.OnImageClickListener;
public class AlbumActivity extends AppCompatActivity {
public static final String TAG = "AlbumActivity";
private static final int PERMISSION_REQUEST_CODE = 101;
public static final String EXTRA_ALBUM_PATH = "album_path";
public static final String EXTRA_ALBUM_NAME = "album_name";
private RecyclerView recyclerView;
private ImageAdapter adapter;
private String albumPath;
private String albumName;
private FloatingActionButton fabScrollTop;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.d(TAG, "onCreate");
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME);
getSupportActionBar().setTitle(albumName);
prefs = new Preferences(this);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
adapter = new ImageAdapter();
adapter.setContext(this);
adapter.setAlbumPath(albumPath);
recyclerView.setAdapter(adapter);
fabScrollTop = findViewById(R.id.fab_scroll_top);
fabScrollTop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recyclerView.scrollToPosition(0);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int firstVisible = layoutManager.findFirstVisibleItemPosition();
if (firstVisible > 0) {
fabScrollTop.setVisibility(View.VISIBLE);
} else {
fabScrollTop.setVisibility(View.GONE);
}
}
}
});
adapter.setOnImageClickListener(new OnImageClickListener() {
@Override
public void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths) {
Intent intent = new Intent(AlbumActivity.this, ImageViewerActivity.class);
intent.putParcelableArrayListExtra(ImageViewerActivity.EXTRA_IMAGE_URLS, urls);
intent.putStringArrayListExtra(ImageViewerActivity.EXTRA_POSITIONS, paths);
intent.putExtra(ImageViewerActivity.EXTRA_POSITION, position);
startActivity(intent);
}
});
if (checkPermission()) {
loadImages();
} else {
requestPermission();
}
}
private boolean checkPermission() {
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadImages();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
}
private String getSortOrder(int sortMode) {
switch (sortMode) {
case Preferences.SORT_TIME_ASC:
return android.provider.MediaStore.Images.Media.DATE_ADDED + " ASC";
case Preferences.SORT_NAME_DESC:
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " DESC";
case Preferences.SORT_NAME_ASC:
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " ASC";
case Preferences.SORT_TIME_DESC:
default:
return android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
}
}
private void loadImages() {
LogUtils.d(TAG, "loadImages");
ArrayList<Uri> imageUrls = new ArrayList<>();
ArrayList<String> imagePaths = new ArrayList<>();
ContentResolver contentResolver = getContentResolver();
Uri collection = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String selection = android.provider.MediaStore.Images.Media.DATA + " LIKE ?";
String[] selectionArgs = new String[]{albumPath + "%"};
int sortMode = prefs.getAlbumSortMode();
String sortOrder = getSortOrder(sortMode);
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
if (cursor != null) {
int dataColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
while (cursor.moveToNext()) {
String path = cursor.getString(dataColumn);
if (path != null && path.startsWith(albumPath + "/")) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID));
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
imageUrls.add(contentUri);
imagePaths.add(path);
}
}
}
}
if (imageUrls.isEmpty()) {
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "No images found");
}
adapter.setData(imageUrls, imagePaths);
LogUtils.d(TAG, "Loaded " + imageUrls.size() + " images");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_album, menu);
int sortMode = prefs.getAlbumSortMode();
int menuId = getSortMenuId(sortMode);
MenuItem item = menu.findItem(menuId);
if (item != null) {
item.setChecked(true);
}
MenuItem sortItem = menu.findItem(R.id.action_sort);
if (sortItem != null && sortItem.getSubMenu() != null) {
sortItem.getSubMenu().setGroupCheckable(0, true, true);
}
return true;
}
private int getSortMenuId(int sortMode) {
switch (sortMode) {
case Preferences.SORT_TIME_ASC:
return R.id.sort_time_asc;
case Preferences.SORT_NAME_DESC:
return R.id.sort_name_desc;
case Preferences.SORT_NAME_ASC:
return R.id.sort_name_asc;
case Preferences.SORT_TIME_DESC:
default:
return R.id.sort_time_desc;
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.sort_time_desc) {
prefs.setAlbumSortMode(Preferences.SORT_TIME_DESC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_time_asc) {
prefs.setAlbumSortMode(Preferences.SORT_TIME_ASC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_name_desc) {
prefs.setAlbumSortMode(Preferences.SORT_NAME_DESC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_name_asc) {
prefs.setAlbumSortMode(Preferences.SORT_NAME_ASC);
item.setChecked(true);
loadImages();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
if (checkPermission()) {
loadImages();
}
if (adapter != null) {
adapter.refreshBg();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == RESULT_OK) {
loadImages();
}
}
}

View File

@@ -1,255 +0,0 @@
package cc.winboll.studio.gallery;
import android.net.Uri;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import cc.winboll.studio.libappbase.LogUtils;
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
public static final String TAG = "AlbumAdapter";
private ArrayList<Album> albums = new ArrayList<>();
private OnAlbumClickListener listener;
private Preferences prefs;
private int bgType = 0;
private PinnedAlbumDbHelper pinnedDbHelper;
private OnCoverSizeListener coverSizeListener;
private boolean coverSizeReported = false;
private int getBgRes() {
switch (bgType) {
case 0:
return R.drawable.bg_checkerboard;
case 1:
return R.drawable.bg_white;
case 2:
return R.drawable.bg_black;
default:
return R.drawable.bg_checkerboard;
}
}
public interface OnAlbumClickListener {
void onAlbumClick(Album album);
}
public interface OnCoverSizeListener {
void onCoverSize(int width, int height);
}
public static final int DEFAULT_COVER_WIDTH = 240;
public static final int DEFAULT_COVER_HEIGHT = 120;
public void setOnAlbumClickListener(OnAlbumClickListener listener) {
this.listener = listener;
}
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
this.coverSizeListener = listener;
}
public void setData(ArrayList<Album> albums) {
this.albums = sortAlbums(albums);
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
notifyDataSetChanged();
}
private ArrayList<Album> sortAlbums(ArrayList<Album> list) {
if (pinnedDbHelper == null || list == null || list.isEmpty()) {
return list;
}
ArrayList<Album> pinned = new ArrayList<>();
ArrayList<Album> unpinned = new ArrayList<>();
for (Album album : list) {
if (pinnedDbHelper.isPinned(album.getPath())) {
pinned.add(album);
} else {
unpinned.add(album);
}
}
pinned.addAll(unpinned);
return pinned;
}
public void setContext(android.content.Context context) {
prefs = new Preferences(context);
bgType = prefs.getBgType();
pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context);
}
public void refreshBg() {
if (prefs != null) {
bgType = prefs.getBgType();
}
notifyDataSetChanged();
}
public void refreshCover() {
notifyDataSetChanged();
}
public void refreshPinned() {
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
ArrayList<Album> pinned = new ArrayList<>();
ArrayList<Album> unpinned = new ArrayList<>();
for (Album album : albums) {
if (pinnedDbHelper.isPinned(album.getPath())) {
pinned.add(album);
} else {
unpinned.add(album);
}
}
Comparator<Album> nameComparator = new Comparator<Album>() {
@Override
public int compare(Album a1, Album a2) {
return a1.getName().compareToIgnoreCase(a2.getName());
}
};
Collections.sort(pinned, nameComparator);
Collections.sort(unpinned, nameComparator);
albums.clear();
albums.addAll(pinned);
albums.addAll(unpinned);
notifyDataSetChanged();
}
}
private void showContextMenu(View view, final Album album) {
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
builder.setTitle(album.getName());
final boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
String[] items = isPinned ? new String[]{"取消置顶"} : new String[]{"置顶"};
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
if (pinnedDbHelper != null) {
if (which == 0) {
if (isPinned) {
pinnedDbHelper.unpinAlbum(album.getPath());
} else {
pinnedDbHelper.pinAlbum(album.getPath());
}
refreshPinned();
}
}
}
});
builder.show();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_album, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
final Album album = albums.get(position);
LogUtils.d(TAG, "bind: " + album.getName() + ", cover=" + album.getCoverUri());
holder.coverImage.setBackgroundResource(getBgRes());
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showContextMenu(holder.itemView, album);
return true;
}
});
holder.albumName.setText(album.getName());
holder.imageCount.setText(album.getImageCount() + " photos");
holder.itemView.post(new Runnable() {
@Override
public void run() {
if (!coverSizeReported && prefs != null) {
int savedWidth = prefs.getCoverWidth();
if (savedWidth == AlbumAdapter.DEFAULT_COVER_WIDTH) {
int width = holder.coverImage.getWidth();
int height = holder.coverImage.getHeight();
if (width > 0 && height > 0) {
prefs.setCoverWidth(width);
prefs.setCoverHeight(height);
float ratio = (float) width / height;
prefs.setCoverRatio(ratio);
coverSizeReported = true;
if (coverSizeListener != null) {
coverSizeListener.onCoverSize(width, height);
}
}
}
}
}
});
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onAlbumClick(album);
}
}
});
Uri coverUri = album.getCoverUri();
if (coverUri != null) {
String uriString = coverUri.toString();
LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString);
if ("file".equals(coverUri.getScheme())) {
File coverFile = new File(coverUri.getPath());
if (coverFile.exists()) {
Glide.with(holder.coverImage.getContext())
.load(coverFile)
.fitCenter()
.into(holder.coverImage);
return;
}
}
Glide.with(holder.coverImage.getContext())
.load(coverUri)
.fitCenter()
.into(holder.coverImage);
}
}
@Override
public int getItemCount() {
return albums.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView coverImage;
ImageView pinIcon;
TextView albumName;
TextView imageCount;
ViewHolder(View itemView) {
super(itemView);
coverImage = itemView.findViewById(R.id.album_cover);
pinIcon = itemView.findViewById(R.id.pin_icon);
albumName = itemView.findViewById(R.id.album_name);
imageCount = itemView.findViewById(R.id.image_count);
}
}
}

View File

@@ -1,131 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.libappbase.LogUtils;
public class AlbumCoverDbHelper extends SQLiteOpenHelper {
public static final String TAG = "AlbumCoverDbHelper";
private static final String DB_NAME = "album_cover.db";
private static final int DB_VERSION = 2;
private static final String TABLE_NAME = "album_covers";
private static final String COLUMN_ALBUM_PATH = "album_path";
private static final String COLUMN_IMAGE_PATH = "image_path";
private static final String COLUMN_CROP_PATH = "crop_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, "
+ COLUMN_IMAGE_PATH + " TEXT, "
+ COLUMN_CROP_PATH + " TEXT)";
private static AlbumCoverDbHelper dbHelper;
public static AlbumCoverDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new AlbumCoverDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public AlbumCoverDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
try {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COLUMN_CROP_PATH + " TEXT");
} catch (Exception e) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
}
public void setCoverWithCrop(String albumPath, String imagePath, String cropPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ALBUM_PATH, albumPath);
values.put(COLUMN_IMAGE_PATH, imagePath);
values.put(COLUMN_CROP_PATH, cropPath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
LogUtils.d(TAG, "setCoverWithCrop: album=" + albumPath + ", image=" + imagePath + ", crop=" + cropPath);
}
public void setCover(String albumPath, String imagePath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ALBUM_PATH, albumPath);
values.put(COLUMN_IMAGE_PATH, imagePath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
LogUtils.d(TAG, "setCover: album=" + albumPath + ", image=" + imagePath);
}
public String getCover(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH, COLUMN_IMAGE_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String coverPath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
coverPath = cursor.getString(0);
if (coverPath == null) {
coverPath = cursor.getString(1);
}
}
cursor.close();
}
return coverPath;
}
public String getCropPath(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String cropPath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
cropPath = cursor.getString(0);
}
cursor.close();
}
return cropPath;
}
public String getOriginalImagePath(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_IMAGE_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String imagePath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
imagePath = cursor.getString(0);
}
cursor.close();
}
return imagePath;
}
public void clearCover(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.putNull(COLUMN_CROP_PATH);
db.update(TABLE_NAME, values, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "clearCover: " + albumPath);
}
public void deleteCover(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "deleteCover: " + albumPath);
}
}

View File

@@ -1,64 +0,0 @@
package cc.winboll.studio.gallery;
import android.app.Dialog;
import android.content.Context;
import android.view.View;
import android.widget.RadioButton;
import android.widget.RadioGroup;
public class BgSelectorDialog extends Dialog {
public interface OnBgSelectedListener {
void onBgSelected(int bgType);
}
private int currentBgType;
private OnBgSelectedListener listener;
public BgSelectorDialog(Context context, int currentBgType) {
super(context);
this.currentBgType = currentBgType;
setContentView(R.layout.dialog_bg_selector);
initViews();
}
private void initViews() {
RadioGroup radioGroup = findViewById(R.id.radio_group_bg);
RadioButton radioCheckerboard = findViewById(R.id.radio_checkerboard);
RadioButton radioWhite = findViewById(R.id.radio_white);
RadioButton radioBlack = findViewById(R.id.radio_black);
switch (currentBgType) {
case 0:
radioCheckerboard.setChecked(true);
break;
case 1:
radioWhite.setChecked(true);
break;
case 2:
radioBlack.setChecked(true);
break;
}
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
int which = 0;
if (checkedId == R.id.radio_white) {
which = 1;
} else if (checkedId == R.id.radio_black) {
which = 2;
}
if (listener != null) {
listener.onBgSelected(which);
}
dismiss();
}
});
}
public void setOnBgSelectedListener(OnBgSelectedListener listener) {
this.listener = listener;
}
}

View File

@@ -1,342 +0,0 @@
package cc.winboll.studio.gallery;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class CropActivity extends AppCompatActivity {
public static final String TAG = "CropActivity";
public static final String EXTRA_IMAGE_URI = "image_uri";
public static final String EXTRA_IMAGE_PATH = "image_path";
public static final String EXTRA_ALBUM_PATH = "album_path";
public static final String EXTRA_CROP_WIDTH = "crop_width";
public static final String EXTRA_CROP_HEIGHT = "crop_height";
private CropCanvasView cropCanvasView;
private ZoomContainerView zoomContainer;
private Bitmap originalBitmap;
private String imagePath;
private String albumPath;
private int cropWidth = 240;
private int cropHeight = 120;
private float cropRatio = 2.0f;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crop);
imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
cropWidth = getIntent().getIntExtra(EXTRA_CROP_WIDTH, 240);
cropHeight = getIntent().getIntExtra(EXTRA_CROP_HEIGHT, 120);
prefs = new Preferences(this);
int bgType = prefs.getBgType();
if (cropWidth > 0 && cropHeight > 0) {
cropRatio = (float) cropWidth / cropHeight;
} else {
cropRatio = prefs.getCoverRatio();
cropWidth = (int) (120 * cropRatio);
cropHeight = 120;
}
findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
final ImageView btnBg = findViewById(R.id.btn_color_pick);
btnBg.setImageResource(R.drawable.ic_bg);
btnBg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showBgDialog();
}
});
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveCroppedCover();
}
});
findViewById(R.id.btn_info).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showCropInfoDialog();
}
});
zoomContainer = findViewById(R.id.zoom_container);
SeekBar seekBarZoom = findViewById(R.id.seekbar_zoom);
seekBarZoom.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (zoomContainer != null && fromUser) {
float scale = 0.1f + (progress / 100f) * 4.9f;
zoomContainer.setScaleFactor(scale);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
cropCanvasView = findViewById(R.id.crop_canvas_view);
final View colorView = findViewById(R.id.color_view);
final GradientDrawable colorDrawable = (GradientDrawable) colorView.getBackground();
cropCanvasView.setBackgroundType(bgType);
updateColorView(colorDrawable, bgType);
cropCanvasView.setOnBackgroundColorChangedListener(new CropCanvasView.OnBackgroundColorChangedListener() {
@Override
public void onBackgroundColorChanged(int color) {
}
});
loadImage();
}
private void updateColorView(GradientDrawable drawable, int bgType) {
switch (bgType) {
case 0:
drawable.setColor(0xFF808080);
break;
case 1:
drawable.setColor(0xFFFFFFFF);
break;
case 2:
default:
drawable.setColor(0xFF000000);
break;
}
}
private void showBgDialog() {
final View colorView = findViewById(R.id.color_view);
final GradientDrawable colorDrawable = (GradientDrawable) colorView.getBackground();
final int currentBgType = cropCanvasView.getBackgroundType();
BgSelectorDialog dialog = new BgSelectorDialog(this, currentBgType);
dialog.setOnBgSelectedListener(new BgSelectorDialog.OnBgSelectedListener() {
@Override
public void onBgSelected(int which) {
cropCanvasView.setBackgroundType(which);
prefs.setBgType(which);
updateColorView(colorDrawable, which);
}
});
dialog.show();
}
private void loadImage() {
try {
if (imagePath != null && new File(imagePath).exists()) {
originalBitmap = BitmapFactory.decodeFile(imagePath);
} else {
Uri imageUri = getIntent().getParcelableExtra(EXTRA_IMAGE_URI);
if (imageUri != null) {
originalBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
}
}
if (originalBitmap != null) {
cropCanvasView.setImageBitmap(originalBitmap);
cropCanvasView.post(new Runnable() {
@Override
public void run() {
initCrop();
}
});
} else {
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
finish();
}
} catch (Exception e) {
LogUtils.e(TAG, "loadImage error: " + e.getMessage());
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
finish();
}
}
private void initCrop() {
cropCanvasView.initCanvas(originalBitmap.getWidth(), originalBitmap.getHeight(), cropRatio);
int viewW = cropCanvasView.getWidth();
int viewH = cropCanvasView.getHeight();
if (viewW > 0 && viewH > 0) {
cropCanvasView.scaleToView(viewW, viewH);
}
}
private void saveCroppedCover() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
return;
}
try {
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
if (canvasBitmap == null || canvasBitmap.isRecycled()) {
Toast.makeText(this, "Failed to get canvas bitmap", Toast.LENGTH_SHORT).show();
return;
}
RectF cropRect = cropCanvasView.getCropRect();
int bmpX = (int) cropRect.left;
int bmpY = (int) cropRect.top;
int bmpW = (int) cropRect.width();
int bmpH = (int) cropRect.height();
bmpX = Math.max(0, Math.min(bmpX, canvasBitmap.getWidth() - 1));
bmpY = Math.max(0, Math.min(bmpY, canvasBitmap.getHeight() - 1));
bmpW = Math.max(1, Math.min(bmpW, canvasBitmap.getWidth() - bmpX));
bmpH = Math.max(1, Math.min(bmpH, canvasBitmap.getHeight() - bmpY));
LogUtils.d(TAG, "saveCroppedCover: cropRect=" + cropRect);
LogUtils.d(TAG, "saveCroppedCover: crop=(" + bmpX + "," + bmpY + "," + bmpW + "," + bmpH + ")");
LogUtils.d(TAG, "saveCroppedCover: canvas size=" + canvasBitmap.getWidth() + "x" + canvasBitmap.getHeight());
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
canvasBitmap.recycle();
LogUtils.d(TAG, "saveCroppedCover: cropped size=" + cropped.getWidth() + "x" + cropped.getHeight());
File coverDir = new File(getFilesDir(), "covers");
if (!coverDir.exists()) {
coverDir.mkdirs();
}
File coverFile = new File(coverDir, "cover_" + System.currentTimeMillis() + ".png");
LogUtils.d(TAG, "saveCroppedCover: cover file=" + coverFile.getAbsolutePath());
FileOutputStream fos = new FileOutputStream(coverFile);
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.close();
LogUtils.d(TAG, "saveCroppedCover: file exists=" + coverFile.exists() + ", length=" + coverFile.length());
cropped.recycle();
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
LogUtils.d(TAG, "saveCroppedCover: cover saved to db, albumPath=" + albumPath);
Intent broadcastIntent = new Intent(Preferences.ACTION_COVER_UPDATED);
sendBroadcast(broadcastIntent);
Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
} catch (Exception e) {
LogUtils.e(TAG, "saveCroppedCover error: " + e.getMessage());
Toast.makeText(this, "Failed to save cover", Toast.LENGTH_SHORT).show();
}
}
private void showCropInfoDialog() {
StringBuilder info = new StringBuilder();
info.append("=== 画布信息 ===\n");
info.append("画布宽度: ").append(cropCanvasView.getCanvasWidth()).append("px\n");
info.append("画布高度: ").append(cropCanvasView.getCanvasHeight()).append("px\n");
info.append("\n=== 背景类型 ===\n");
String[] bgNames = {"灰白相间", "全白", "全黑"};
info.append("背景: ").append(bgNames[cropCanvasView.getBackgroundType()]).append("\n");
int bgColor = cropCanvasView.getBackgroundColor();
info.append("背景颜色: #").append(String.format("%06X", bgColor & 0xFFFFFF)).append("\n");
info.append("拾取坐标: ").append(cropCanvasView.getLastPickImageX()).append(",")
.append(cropCanvasView.getLastPickImageY()).append("\n");
info.append("\n=== 裁剪结果 ===\n");
RectF cropRect = cropCanvasView.getCropRect();
if (cropRect != null) {
info.append("裁剪区域: ").append((int)cropRect.left).append(",")
.append((int)cropRect.top).append(",")
.append((int)cropRect.right).append(",")
.append((int)cropRect.bottom).append("\n");
info.append("裁剪宽度: ").append((int)cropRect.width()).append("px\n");
info.append("裁剪高度: ").append((int)cropRect.height()).append("px\n");
}
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
final Bitmap[] previewHolder = new Bitmap[1];
if (canvasBitmap != null && !canvasBitmap.isRecycled() && cropRect != null) {
int bmpX = Math.max(0, (int) cropRect.left);
int bmpY = Math.max(0, (int) cropRect.top);
int bmpW = Math.min((int) cropRect.width(), canvasBitmap.getWidth() - bmpX);
int bmpH = Math.min((int) cropRect.height(), canvasBitmap.getHeight() - bmpY);
if (bmpW > 0 && bmpH > 0) {
previewHolder[0] = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
}
}
final Bitmap previewBitmap = previewHolder[0];
View dialogView = getLayoutInflater().inflate(R.layout.dialog_crop_info, null);
TextView infoText = dialogView.findViewById(R.id.info_text);
ImageView previewImage = dialogView.findViewById(R.id.preview_image);
infoText.setText(info.toString());
if (previewBitmap != null) {
previewImage.setImageBitmap(previewBitmap);
}
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("裁剪信息")
.setView(dialogView)
.setPositiveButton("确定", null)
.create();
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (previewBitmap != null && !previewBitmap.isRecycled()) {
previewBitmap.recycle();
}
}
});
dialog.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (originalBitmap != null) {
originalBitmap.recycle();
originalBitmap = null;
}
}
}

View File

@@ -1,750 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class CropCanvasView extends View {
public interface OnBackgroundColorChangedListener {
void onBackgroundColorChanged(int color);
}
public interface OnColorPickedListener {
void onColorPicked(int color);
}
public interface OnColorPickEndListener {
void onColorPickEnd();
}
private OnBackgroundColorChangedListener backgroundColorChangedListener;
private OnColorPickedListener colorPickedListener;
private OnColorPickEndListener colorPickEndListener;
public void setOnBackgroundColorChangedListener(OnBackgroundColorChangedListener listener) {
this.backgroundColorChangedListener = listener;
}
public void setOnColorPickedListener(OnColorPickedListener listener) {
this.colorPickedListener = listener;
}
public void setOnColorPickEndListener(OnColorPickEndListener listener) {
this.colorPickEndListener = listener;
}
private Paint imagePaint;
private Paint borderPaint;
private Paint cornerPaint;
private RectF cropRect;
private int touchArea = 50;
private float lastX, lastY;
private int activeCorner = -1;
private static final int CORNER_TOP_LEFT = 0;
private static final int CORNER_TOP_RIGHT = 1;
private static final int CORNER_BOTTOM_LEFT = 2;
private static final int CORNER_BOTTOM_RIGHT = 3;
private static final int CORNER_CENTER = 4;
private int imageWidth;
private int imageHeight;
private float coverRatio = 2.0f;
private int extendHeight;
private int extendWidth;
private int canvasWidth;
private int canvasHeight;
private float minSize = 50;
private RectF imageBounds = new RectF();
private RectF canvasBounds = new RectF();
private Bitmap originalBitmap;
private Bitmap canvasBitmap;
private float bitmapScale = 1.0f;
private Bitmap displayBitmap;
private RectF initialSpanRect;
private float initialSpan;
private int bgType = 2;
private Bitmap tileBitmap;
private BitmapShader tileShader;
private Paint bgPaint;
private int backgroundColor = Color.BLUE;
private boolean colorPickMode = false;
private int previewColor = 0;
private float lastPickX = 0;
private float lastPickY = 0;
private float pickX, pickY;
private float displayScale = 1.0f;
private float displayOffsetX = 0f;
private float displayOffsetY = 0f;
private static final int MAX_DISPLAY_SIZE = 2048;
private float containerScale = 1.0f;
public CropCanvasView(Context context) {
super(context);
init();
}
public CropCanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CropCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
imagePaint = new Paint();
imagePaint.setFilterBitmap(true);
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#CCAA00"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(3);
cornerPaint = new Paint();
cornerPaint.setColor(Color.WHITE);
cornerPaint.setStyle(Paint.Style.FILL);
bgType = 2;
bgPaint = new Paint();
initTileBitmap();
}
private void initTileBitmap() {
if (tileBitmap != null && !tileBitmap.isRecycled()) {
tileBitmap.recycle();
tileBitmap = null;
}
tileShader = null;
if (bgType == 0) {
Drawable drawable = getContext().getDrawable(R.drawable.bg_checkerboard);
if (drawable != null) {
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
if (w <= 0) w = 10;
if (h <= 0) h = 10;
tileBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(tileBitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(c);
tileShader = new BitmapShader(tileBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
}
}
}
public void setBackgroundType(int type) {
if (bgType != type) {
bgType = type;
initTileBitmap();
invalidate();
}
}
public int getBackgroundType() {
return bgType;
}
private void drawBackground(Canvas canvas) {
if (bgType == 0 && tileShader != null) {
bgPaint.setShader(tileShader);
canvas.drawRect(canvasBounds, bgPaint);
} else if (bgType == 1) {
bgPaint.setShader(null);
bgPaint.setColor(Color.WHITE);
canvas.drawRect(canvasBounds, bgPaint);
} else {
bgPaint.setShader(null);
bgPaint.setColor(Color.BLACK);
canvas.drawRect(canvasBounds, bgPaint);
}
}
public void setImageBitmap(Bitmap bitmap) {
if (displayBitmap != null && displayBitmap != originalBitmap) {
displayBitmap.recycle();
}
this.originalBitmap = bitmap;
createDisplayBitmap();
}
private void createDisplayBitmap() {
if (displayBitmap != null && displayBitmap != originalBitmap) {
displayBitmap.recycle();
displayBitmap = null;
}
if (originalBitmap == null || originalBitmap.isRecycled()) {
return;
}
int w = originalBitmap.getWidth();
int h = originalBitmap.getHeight();
float scale = 1.0f;
if (w > MAX_DISPLAY_SIZE || h > MAX_DISPLAY_SIZE) {
scale = Math.min((float) MAX_DISPLAY_SIZE / w, (float) MAX_DISPLAY_SIZE / h);
int newW = (int) (w * scale);
int newH = (int) (h * scale);
displayBitmap = Bitmap.createScaledBitmap(originalBitmap, newW, newH, true);
} else {
displayBitmap = originalBitmap;
}
displayBitmapScale = scale;
}
private float displayBitmapScale = 1.0f;
public Bitmap getOriginalBitmap() {
return originalBitmap;
}
public float getDisplayBitmapScale() {
return displayBitmapScale;
}
public void onContainerScaled() {
containerScale = 1.0f;
invalidate();
}
public void setContainerScale(float scale) {
if (scale > 0 && scale != containerScale) {
containerScale = scale;
invalidate();
}
}
public float getContainerScale() {
return containerScale;
}
public Bitmap getCanvasBitmap() {
if (canvasWidth <= 0 || canvasHeight <= 0) {
return null;
}
Bitmap canvasBmp = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(canvasBmp);
drawBackground(canvas);
if (displayBitmap != null && !displayBitmap.isRecycled()) {
if (displayBitmap == originalBitmap) {
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
} else {
float invScale = 1f / displayBitmapScale;
canvas.save();
canvas.scale(invScale, invScale);
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
imageBounds.top * displayBitmapScale, imagePaint);
canvas.restore();
}
}
return canvasBmp;
}
public void initCanvas(int imgWidth, int imgHeight, float ratio) {
this.imageWidth = imgWidth;
this.imageHeight = imgHeight;
this.coverRatio = ratio;
float imgRatio = (float) imgWidth / imgHeight;
if (coverRatio >= imgRatio) {
extendHeight = imgHeight;
extendWidth = (int) (imgHeight * coverRatio);
} else {
extendWidth = imgWidth;
extendHeight = (int) (imgWidth / coverRatio);
}
canvasHeight = Math.max(imageHeight, extendHeight);
canvasWidth = Math.max(imageWidth, extendWidth);
float left = (canvasWidth - imageWidth) / 2f;
float top = (canvasHeight - imageHeight) / 2f;
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
cropRect = new RectF(canvasBounds);
requestLayout();
invalidate();
}
public void getDisplayMatrix(Matrix matrix) {
if (canvasWidth <= 0 || canvasHeight <= 0 || getWidth() <= 0 || getHeight() <= 0) {
return;
}
matrix.reset();
float displayScaleX = (float) getWidth() / canvasWidth;
float displayScaleY = (float) getHeight() / canvasHeight;
displayScale = Math.min(displayScaleX, displayScaleY);
displayOffsetX = (getWidth() - canvasWidth * displayScale) / 2f;
displayOffsetY = (getHeight() - canvasHeight * displayScale) / 2f;
matrix.postTranslate(displayOffsetX, displayOffsetY);
matrix.postScale(displayScale, displayScale);
}
public float screenToImageX(float screenX) {
return (screenX - displayOffsetX) / displayScale;
}
public float screenToImageY(float screenY) {
return (screenY - displayOffsetY) / displayScale;
}
public float imageToScreenX(float imageX) {
return imageX * displayScale + displayOffsetX;
}
public float imageToScreenY(float imageY) {
return imageY * displayScale + displayOffsetY;
}
public int getImageColorAt(float screenX, float screenY) {
Bitmap bmp = (displayBitmap != null) ? displayBitmap : originalBitmap;
if (bmp == null || bmp.isRecycled()) {
return Color.TRANSPARENT;
}
float imgX = screenToImageX(screenX);
float imgY = screenToImageY(screenY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
if (bmpX >= 0 && bmpX < bmp.getWidth() &&
bmpY >= 0 && bmpY < bmp.getHeight()) {
return bmp.getPixel(bmpX, bmpY);
}
return Color.TRANSPARENT;
}
public void setBackgroundColor(int color) {
this.backgroundColor = color;
if (backgroundColorChangedListener != null) {
backgroundColorChangedListener.onBackgroundColorChanged(color);
}
invalidate();
}
public int getBackgroundColor() {
return backgroundColor;
}
public int getPreviewColor() {
return previewColor;
}
public float getLastPickX() {
return lastPickX;
}
public float getLastPickY() {
return lastPickY;
}
public int getLastPickImageX() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
return 0;
}
float imgX = screenToImageX(lastPickX);
float imgY = screenToImageY(lastPickY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
return Math.max(0, Math.min(bmpX, originalBitmap.getWidth() - 1));
}
public int getLastPickImageY() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
return 0;
}
float imgX = screenToImageX(lastPickX);
float imgY = screenToImageY(lastPickY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
return Math.max(0, Math.min(bmpY, originalBitmap.getHeight() - 1));
}
public void setColorPickMode(boolean enable) {
this.colorPickMode = enable;
if (enable) {
pickX = -1;
pickY = -1;
}
invalidate();
}
public boolean isColorPickMode() {
return colorPickMode;
}
public void scaleToView(int viewWidth, int viewHeight) {
if (viewWidth > 0 && viewHeight > 0 && canvasWidth > 0 && canvasHeight > 0) {
float scale = Math.max((float) viewWidth / canvasWidth, (float) viewHeight / canvasHeight);
int oldCanvasW = canvasWidth;
int oldCanvasH = canvasHeight;
canvasWidth = (int) (canvasWidth * scale);
canvasHeight = (int) (canvasHeight * scale);
float left = (canvasWidth - imageWidth) / 2f;
float top = (canvasHeight - imageHeight) / 2f;
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
if (cropRect != null) {
float scaleX = (float) canvasWidth / oldCanvasW;
float scaleY = (float) canvasHeight / oldCanvasH;
cropRect.left *= scaleX;
cropRect.top *= scaleY;
cropRect.right *= scaleX;
cropRect.bottom *= scaleY;
}
requestLayout();
invalidate();
}
}
public RectF getCropRect() {
return new RectF(cropRect);
}
public int getCanvasWidth() {
return canvasWidth;
}
public int getCanvasHeight() {
return canvasHeight;
}
public RectF getCanvasBounds() {
return new RectF(canvasBounds);
}
public RectF getImageBounds() {
return new RectF(imageBounds);
}
public float getBitmapScale() {
return displayScale;
}
public float getDisplayScale() {
return displayScale;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = canvasWidth > 0 ? canvasWidth : 0;
int h = canvasHeight > 0 ? canvasHeight : 0;
setMeasuredDimension(w, h);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
getDisplayMatrix(new Matrix());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (cropRect == null) return;
if (displayBitmap != null && !displayBitmap.isRecycled()) {
canvas.save();
Matrix matrix = new Matrix();
getDisplayMatrix(matrix);
canvas.concat(matrix);
drawBackground(canvas);
if (displayBitmap == originalBitmap) {
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
} else {
float invScale = 1f / displayBitmapScale;
canvas.save();
canvas.scale(invScale, invScale);
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
imageBounds.top * displayBitmapScale, imagePaint);
canvas.restore();
}
canvas.restore();
if (containerScale != 1.0f) {
canvas.save();
canvas.scale(containerScale, containerScale);
canvas.drawRect(cropRect, borderPaint);
float cornerRadius = 12f / containerScale;
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
canvas.restore();
} else {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
} else {
if (containerScale != 1.0f) {
canvas.save();
canvas.scale(containerScale, containerScale);
canvas.drawRect(cropRect, borderPaint);
float cornerRadius = 12f / containerScale;
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
canvas.restore();
} else {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (cropRect == null || canvasBounds.isEmpty()) return super.onTouchEvent(event);
if (event.getPointerCount() == 2) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_POINTER_DOWN) {
initialSpan = getSpan(event);
return true;
} else if (action == MotionEvent.ACTION_MOVE) {
float span = getSpan(event);
if (initialSpan > 0) {
float scale = span / initialSpan;
float centerX = cropRect.centerX();
float centerY = cropRect.centerY();
float newWidth = cropRect.width() * scale;
float newHeight = newWidth / coverRatio;
newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width()));
newHeight = newWidth / coverRatio;
float newLeft = centerX - newWidth / 2;
float newTop = centerY - newHeight / 2;
float newRight = centerX + newWidth / 2;
float newBottom = centerY + newHeight / 2;
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right &&
newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
cropRect.set(newLeft, newTop, newRight, newBottom);
}
initialSpan = span;
invalidate();
}
return true;
}
}
float x = event.getX();
float y = event.getY();
RectF bounds = canvasBounds;
if (containerScale != 1.0f) {
x = x / containerScale;
y = y / containerScale;
bounds = new RectF(
canvasBounds.left,
canvasBounds.top,
canvasBounds.right,
canvasBounds.bottom
);
}
if (colorPickMode) {
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
if (originalBitmap != null && !originalBitmap.isRecycled()) {
float imgX = screenToImageX(x);
float imgY = screenToImageY(y);
if (imageBounds.contains(imgX, imgY)) {
previewColor = getImageColorAt(x, y);
} else if (canvasBounds.contains(x, y)) {
previewColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
} else {
previewColor = Color.TRANSPARENT;
}
lastPickX = x;
lastPickY = y;
if (backgroundColorChangedListener != null) {
backgroundColorChangedListener.onBackgroundColorChanged(previewColor);
}
}
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (canvasBounds.contains(x, y)) {
int pickedColor;
float imgX = screenToImageX(x);
float imgY = screenToImageY(y);
if (imageBounds.contains(imgX, imgY)) {
int colorAtPoint = getImageColorAt(x, y);
pickedColor = colorAtPoint;
backgroundColor = colorAtPoint;
} else {
pickedColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
}
if (colorPickedListener != null) {
colorPickedListener.onColorPicked(pickedColor);
}
}
if (colorPickEndListener != null) {
colorPickEndListener.onColorPickEnd();
}
previewColor = 0;
invalidate();
return true;
}
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
activeCorner = getActiveCorner(x, y);
return true;
case MotionEvent.ACTION_MOVE:
float dx = x - lastX;
float dy = y - lastY;
if (activeCorner == CORNER_CENTER) {
float newLeft = cropRect.left + dx;
float newTop = cropRect.top + dy;
float newRight = cropRect.right + dx;
float newBottom = cropRect.bottom + dy;
if (newLeft >= bounds.left && newRight <= bounds.right) {
cropRect.left = newLeft;
cropRect.right = newRight;
}
if (newTop >= bounds.top && newBottom <= bounds.bottom) {
cropRect.top = newTop;
cropRect.bottom = newBottom;
}
} else if (activeCorner == CORNER_TOP_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
} else if (activeCorner == CORNER_TOP_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
}
lastX = x;
lastY = y;
invalidate();
return true;
case MotionEvent.ACTION_UP:
activeCorner = -1;
return true;
}
return super.onTouchEvent(event);
}
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
float newWidth;
float newLeft = cropRect.left;
float newTop = cropRect.top;
float newRight = cropRect.right;
float newBottom = cropRect.bottom;
if (left) {
newWidth = cropRect.width() - (nx - cropRect.left);
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
} else {
newWidth = nx - cropRect.left;
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
newLeft = cropRect.left;
}
float newHeight = newWidth / coverRatio;
if (top) {
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
newBottom = newTop + newHeight;
} else {
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
newTop = newBottom - newHeight;
}
if (left) {
cropRect.left = newLeft;
cropRect.right = newLeft + newWidth;
} else {
cropRect.right = newRight;
cropRect.left = newRight - newWidth;
}
if (top) {
cropRect.top = newTop;
cropRect.bottom = newTop + newHeight;
} else {
cropRect.bottom = newBottom;
cropRect.top = newBottom - newHeight;
}
}
private int getActiveCorner(float x, float y) {
float dx = Math.abs(x - cropRect.left);
float dy = Math.abs(y - cropRect.top);
float dx2 = Math.abs(x - cropRect.right);
float dy2 = Math.abs(y - cropRect.bottom);
if (dx <= touchArea && dy <= touchArea) {
return CORNER_TOP_LEFT;
}
if (dx2 <= touchArea && dy <= touchArea) {
return CORNER_TOP_RIGHT;
}
if (dx <= touchArea && dy2 <= touchArea) {
return CORNER_BOTTOM_LEFT;
}
if (dx2 <= touchArea && dy2 <= touchArea) {
return CORNER_BOTTOM_RIGHT;
}
if (x > cropRect.left && x < cropRect.right && y > cropRect.top && y < cropRect.bottom) {
return CORNER_CENTER;
}
return -1;
}
private float getSpan(MotionEvent event) {
float x0 = event.getX(0);
float y0 = event.getY(0);
float x1 = event.getX(1);
float y1 = event.getY(1);
return (float) Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
}
public int getColorAt(float x, float y) {
return getImageColorAt(x, y);
}
}

View File

@@ -1,224 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class CropOverlayView extends View {
public interface OnCanvasSizeChangedListener {
void onCanvasSizeChanged(RectF canvasBounds);
}
private OnCanvasSizeChangedListener canvasSizeChangedListener;
private Paint borderPaint;
private Paint cornerPaint;
private RectF cropRect;
private int touchArea = 50;
private float lastX, lastY;
private int activeCorner = -1;
private static final int CORNER_TOP_LEFT = 0;
private static final int CORNER_TOP_RIGHT = 1;
private static final int CORNER_BOTTOM_LEFT = 2;
private static final int CORNER_BOTTOM_RIGHT = 3;
private static final int CORNER_CENTER = 4;
private float targetRatio = 2.0f;
private RectF canvasBounds = new RectF();
private float minSize = 50;
public CropOverlayView(Context context) {
super(context);
initPaints();
}
public CropOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaints();
}
public CropOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaints();
}
private void initPaints() {
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#CCAA00"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(3);
cornerPaint = new Paint();
cornerPaint.setColor(Color.WHITE);
cornerPaint.setStyle(Paint.Style.FILL);
}
public void setTargetRatio(float ratio) {
this.targetRatio = ratio;
}
public float getTargetRatio() {
return targetRatio;
}
public void setOnCanvasSizeChangedListener(OnCanvasSizeChangedListener listener) {
this.canvasSizeChangedListener = listener;
}
public void initCanvas(int canvasW, int canvasH) {
canvasBounds.set(0, 0, canvasW, canvasH);
cropRect = new RectF(0, 0, canvasW, canvasH);
if (canvasSizeChangedListener != null) {
canvasSizeChangedListener.onCanvasSizeChanged(new RectF(canvasBounds));
}
invalidate();
}
public RectF getCropRect() {
return new RectF(cropRect);
}
public RectF getCanvasBounds() {
return new RectF(canvasBounds);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.TRANSPARENT);
if (cropRect != null) {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (cropRect == null || canvasBounds.isEmpty()) return false;
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
activeCorner = getActiveCorner(x, y);
return true;
case MotionEvent.ACTION_MOVE:
float dx = x - lastX;
float dy = y - lastY;
if (activeCorner == CORNER_CENTER) {
float newLeft = cropRect.left + dx;
float newTop = cropRect.top + dy;
float newRight = cropRect.right + dx;
float newBottom = cropRect.bottom + dy;
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) {
cropRect.left = newLeft;
cropRect.right = newRight;
}
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
cropRect.top = newTop;
cropRect.bottom = newBottom;
}
} else if (activeCorner == CORNER_TOP_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
} else if (activeCorner == CORNER_TOP_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
}
lastX = x;
lastY = y;
invalidate();
return true;
case MotionEvent.ACTION_UP:
activeCorner = -1;
return true;
}
return super.onTouchEvent(event);
}
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
float newWidth;
float newLeft = cropRect.left;
float newTop = cropRect.top;
float newRight = cropRect.right;
float newBottom = cropRect.bottom;
if (left) {
newWidth = cropRect.width() - (nx - cropRect.left);
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
} else {
newWidth = nx - cropRect.left;
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
newLeft = cropRect.left;
}
float newHeight = newWidth / targetRatio;
if (top) {
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
newBottom = newTop + newHeight;
} else {
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
newTop = newBottom - newHeight;
}
if (left) {
cropRect.left = newLeft;
cropRect.right = newLeft + newWidth;
} else {
cropRect.right = newRight;
cropRect.left = newRight - newWidth;
}
if (top) {
cropRect.top = newTop;
cropRect.bottom = newTop + newHeight;
} else {
cropRect.bottom = newBottom;
cropRect.top = newBottom - newHeight;
}
}
private int getActiveCorner(float x, float y) {
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
return CORNER_TOP_LEFT;
}
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
return CORNER_TOP_RIGHT;
}
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
return CORNER_BOTTOM_LEFT;
}
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
return CORNER_BOTTOM_RIGHT;
}
if (cropRect.contains(x, y)) {
return CORNER_CENTER;
}
return -1;
}
}

View File

@@ -1,44 +0,0 @@
package cc.winboll.studio.gallery;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/04/24 15:23
*/
public class GlobalWinBoLLApplication extends GlobalApplication {
public static final String TAG = "GlobalWinBoLLApplication";
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "onCreate");
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
// 设置 Toast 布局样式
//ToastUtils.setView(R.layout.view_toast);
//ToastUtils.setStyle(new WhiteToastStyle());
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
//CrashHandler.getInstance().registerGlobal(this);
//CrashHandler.getInstance().registerPart(this);
}
@Override
public void onTerminate() {
super.onTerminate();
LogUtils.d(TAG, "onTerminate");
ToastUtils.release();
}
}

View File

@@ -1,247 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import cc.winboll.studio.libappbase.LogUtils;
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
public static final String TAG = "ImageAdapter";
private ArrayList<Uri> imageUrls = new ArrayList<>();
private ArrayList<String> imagePaths = new ArrayList<>();
private OnImageClickListener listener;
private int bgType = 0;
private Preferences prefs;
private PinnedImageDbHelper pinnedDbHelper;
private AlbumCoverDbHelper coverDbHelper;
private String albumPath;
private int getBgRes() {
switch (bgType) {
case 0:
return R.drawable.bg_checkerboard;
case 1:
return R.drawable.bg_white;
case 2:
return R.drawable.bg_black;
default:
return R.drawable.bg_checkerboard;
}
}
public interface OnImageClickListener {
void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths);
}
public void setOnImageClickListener(OnImageClickListener listener) {
this.listener = listener;
}
public void setData(ArrayList<Uri> urls, ArrayList<String> paths) {
this.imageUrls = urls;
this.imagePaths = paths;
sortPinnedFirst();
LogUtils.d(TAG, "setData: " + urls.size() + " images");
notifyDataSetChanged();
}
private void sortPinnedFirst() {
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
return;
}
ArrayList<Uri> pinnedUrls = new ArrayList<>();
ArrayList<String> pinnedPaths = new ArrayList<>();
ArrayList<Uri> unpinnedUrls = new ArrayList<>();
ArrayList<String> unpinnedPaths = new ArrayList<>();
for (int i = 0; i < imagePaths.size(); i++) {
String path = imagePaths.get(i);
if (pinnedDbHelper.isPinned(path)) {
pinnedUrls.add(imageUrls.get(i));
pinnedPaths.add(path);
} else {
unpinnedUrls.add(imageUrls.get(i));
unpinnedPaths.add(path);
}
}
imageUrls.clear();
imagePaths.clear();
imageUrls.addAll(pinnedUrls);
imageUrls.addAll(unpinnedUrls);
imagePaths.addAll(pinnedPaths);
imagePaths.addAll(unpinnedPaths);
}
public void setContext(android.content.Context context) {
prefs = new Preferences(context);
bgType = prefs.getBgType();
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
coverDbHelper = AlbumCoverDbHelper.getInstance(context);
}
public void setAlbumPath(String albumPath) {
this.albumPath = albumPath;
}
public int getCropWidth() {
return prefs != null ? prefs.getCoverWidth() : 240;
}
public int getCropHeight() {
return prefs != null ? prefs.getCoverHeight() : 120;
}
public void refreshBg() {
if (prefs != null) {
bgType = prefs.getBgType();
notifyDataSetChanged();
}
}
public void refreshPinned() {
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
return;
}
sortPinnedFirst();
notifyDataSetChanged();
}
private void showContextMenu(final View view, final int position) {
final String imagePath = imagePaths.get(position);
final Uri imageUri = imageUrls.get(position);
final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)};
final boolean[] isCover = {false};
if (coverDbHelper != null && albumPath != null) {
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
isCover[0] = originalPath != null && originalPath.equals(imagePath);
}
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
builder.setTitle("Image");
String[] items;
if (isPinned[0]) {
if (isCover[0]) {
items = new String[]{"取消置顶", "调整封面", "取消封面"};
} else {
items = new String[]{"取消置顶", "设置为封面"};
}
} else {
if (isCover[0]) {
items = new String[]{"置顶", "调整封面", "取消封面"};
} else {
items = new String[]{"置顶", "设置为封面"};
}
}
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
if (which == 0) {
if (pinnedDbHelper != null) {
if (isPinned[0]) {
pinnedDbHelper.unpinImage(imagePath);
} else {
pinnedDbHelper.pinImage(imagePath);
}
refreshPinned();
}
} else if (which == 1) {
if (coverDbHelper != null && albumPath != null) {
Intent cropIntent = new Intent(view.getContext(), CropActivity.class);
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri);
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath);
cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath);
cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth());
cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight());
((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100);
}
} else if (which == 2) {
if (coverDbHelper != null && albumPath != null && isCover[0]) {
coverDbHelper.deleteCover(albumPath);
notifyDataSetChanged();
}
}
}
});
builder.show();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_gallery, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
holder.imageView.setBackgroundResource(getBgRes());
Glide.with(holder.imageView.getContext())
.load(imageUrls.get(position))
.centerCrop()
.into(holder.imageView);
final ArrayList<Uri> urls = imageUrls;
final ArrayList<String> paths = imagePaths;
final String imagePath = imagePaths.get(position);
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
boolean isCover = false;
if (coverDbHelper != null && albumPath != null) {
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
isCover = originalPath != null && originalPath.equals(imagePath);
}
holder.coverIcon.setVisibility(isCover ? View.VISIBLE : View.GONE);
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onImageClick(position, urls, paths);
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showContextMenu(holder.itemView, position);
return true;
}
});
}
@Override
public int getItemCount() {
return imageUrls.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
ImageView pinIcon;
ImageView coverIcon;
ViewHolder(View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image);
pinIcon = itemView.findViewById(R.id.pin_icon);
coverIcon = itemView.findViewById(R.id.cover_icon);
}
}
}

View File

@@ -1,72 +0,0 @@
package cc.winboll.studio.gallery;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.PagerAdapter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class ImagePagerAdapter extends PagerAdapter {
public static final String TAG = "ImagePagerAdapter";
private ArrayList<Uri> imageUrls;
private int bgType;
public ImagePagerAdapter(ArrayList<Uri> imageUrls, int bgType) {
this.imageUrls = imageUrls;
this.bgType = bgType;
LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images, bgType=" + bgType);
}
private int getBgRes() {
switch (bgType) {
case 0:
return R.drawable.bg_checkerboard;
case 1:
return R.drawable.bg_white;
case 2:
return R.drawable.bg_black;
default:
return R.drawable.bg_checkerboard;
}
}
@Override
public int getCount() {
return imageUrls.size();
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
View view = LayoutInflater.from(container.getContext())
.inflate(R.layout.item_image_pager, container, false);
view.setBackgroundResource(getBgRes());
ImageView imageView = view.findViewById(R.id.image);
Glide.with(imageView.getContext())
.load(imageUrls.get(position))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.into(imageView);
container.addView(view);
return view;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View) object);
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
}

View File

@@ -1,452 +0,0 @@
package cc.winboll.studio.gallery;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.viewpager.widget.ViewPager;
import java.io.File;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class ImageViewerActivity extends Activity implements ViewPager.OnPageChangeListener {
public static final String TAG = "ImageViewerActivity";
public static final String EXTRA_IMAGE_URLS = "image_urls";
public static final String EXTRA_POSITIONS = "image_positions";
public static final String EXTRA_POSITION = "position";
private ArrayList<Uri> imageUrls;
private ArrayList<String> imagePaths;
private int currentPosition;
private ViewPager viewPager;
private View toolbar;
private ImageButton btnBack;
private ImageButton btnDelete;
private ImageButton btnShare;
private ImageButton btnInfo;
private ImageButton btnBg;
private ImageButton btnGallery;
private int bgType = 0;
private GestureDetector gestureDetector;
private TrashManager trashManager;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_image_viewer);
LogUtils.d(TAG, "onCreate");
imageUrls = getIntent().getParcelableArrayListExtra(EXTRA_IMAGE_URLS);
imagePaths = getIntent().getStringArrayListExtra(EXTRA_POSITIONS);
currentPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
trashManager = new TrashManager(this);
prefs = new Preferences(this);
bgType = prefs.getBgType();
viewPager = findViewById(R.id.view_pager);
toolbar = findViewById(R.id.toolbar);
btnBack = findViewById(R.id.btn_back);
btnDelete = findViewById(R.id.btn_delete);
btnShare = findViewById(R.id.btn_share);
btnInfo = findViewById(R.id.btn_info);
btnBg = findViewById(R.id.btn_bg);
btnGallery = findViewById(R.id.btn_gallery);
applyBg();
ImagePagerAdapter adapter = new ImagePagerAdapter(imageUrls, bgType);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(currentPosition);
viewPager.addOnPageChangeListener(this);
gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
toggleToolbar();
return true;
}
});
viewPager.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});
btnBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
btnDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDeleteDialog();
}
});
btnShare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
shareCurrentImage();
}
});
btnInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showImageInfo();
}
});
btnBg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
switchBg();
}
});
btnGallery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(ImageViewerActivity.this, "Gallery", Toast.LENGTH_SHORT).show();
if (imageUrls != null && currentPosition >= 0 && currentPosition < imageUrls.size()) {
Uri imageUri = imageUrls.get(currentPosition);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(imageUri, "image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "打开相册"));
}
}
});
}
private void toggleToolbar() {
if (toolbar.getVisibility() == View.VISIBLE) {
toolbar.setVisibility(View.GONE);
} else {
toolbar.setVisibility(View.VISIBLE);
}
}
private void applyBg() {
int bgRes;
switch (bgType) {
case 0:
bgRes = R.drawable.bg_checkerboard;
break;
case 1:
bgRes = R.drawable.bg_white;
break;
case 2:
bgRes = R.drawable.bg_black;
break;
default:
bgRes = R.drawable.bg_checkerboard;
}
View container = findViewById(R.id.container);
if (container != null) {
container.setBackgroundResource(bgRes);
}
}
private void switchBg() {
final int[] bgResources = {R.drawable.bg_checkerboard, R.drawable.bg_white, R.drawable.bg_black};
BgSelectorDialog dialog = new BgSelectorDialog(this, bgType);
dialog.setOnBgSelectedListener(new BgSelectorDialog.OnBgSelectedListener() {
@Override
public void onBgSelected(int which) {
bgType = which;
prefs.setBgType(which);
int currentItem = viewPager.getCurrentItem();
View container = findViewById(R.id.container);
if (container != null) {
container.setBackgroundResource(bgResources[which]);
}
viewPager.setAdapter(new ImagePagerAdapter(imageUrls, bgType));
viewPager.setCurrentItem(currentItem);
}
});
dialog.show();
}
private void showDeleteDialog() {
new AlertDialog.Builder(this)
.setMessage("Delete to trash?")
.setPositiveButton("Yes", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
moveToTrash();
}
})
.setNegativeButton("No", null)
.show();
}
private void moveToTrash() {
LogUtils.d(TAG, "moveToTrash");
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
String imagePath = "";
if (imagePaths != null && currentPosition < imagePaths.size()) {
imagePath = imagePaths.get(currentPosition);
} else {
imagePath = getPathFromUri(imageUrls.get(currentPosition));
}
Uri imageUri = imageUrls.get(currentPosition);
long result = -1;
if (!imagePath.isEmpty()) {
result = trashManager.addToTrash(imagePath);
}
if (result > 0) {
try {
getContentResolver().delete(imageUri, null, null);
} catch (Exception e) {
e.printStackTrace();
}
android.widget.Toast.makeText(this, "Moved to trash", android.widget.Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "Moved to trash");
removeCurrentImage();
} else {
try {
int deleted = getContentResolver().delete(imageUri, null, null);
android.widget.Toast.makeText(this, "Deleted: " + deleted, android.widget.Toast.LENGTH_SHORT).show();
removeCurrentImage();
} catch (Exception e) {
e.printStackTrace();
removeCurrentImage();
}
}
}
}
private void removeCurrentImage() {
imageUrls.remove(currentPosition);
if (imagePaths != null) {
imagePaths.remove(currentPosition);
}
if (imageUrls.isEmpty()) {
finish();
} else {
if (currentPosition >= imageUrls.size()) {
currentPosition = imageUrls.size() - 1;
}
viewPager.setAdapter(new ImagePagerAdapter(imageUrls, bgType));
viewPager.setCurrentItem(currentPosition);
}
}
private String getPathFromUri(Uri uri) {
String[] projection = { MediaStore.Images.Media.DATA };
android.database.Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
return cursor.getString(columnIndex);
}
} finally {
cursor.close();
}
}
return uri.getPath();
}
private void shareCurrentImage() {
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
Uri imageUri = imageUrls.get(currentPosition);
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("image/*");
shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent, "Share Image"));
}
}
private void showImageInfo() {
if (currentPosition < 0 || currentPosition >= imageUrls.size()) {
return;
}
String imagePath = "";
if (imagePaths != null && currentPosition < imagePaths.size()) {
imagePath = imagePaths.get(currentPosition);
} else {
imagePath = getPathFromUri(imageUrls.get(currentPosition));
}
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
imageFile = new File(imagePath);
}
android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
layout.setOrientation(android.widget.LinearLayout.VERTICAL);
layout.setPadding(48, 32, 48, 32);
android.widget.TextView labelPath = new android.widget.TextView(this);
labelPath.setText("Path:");
labelPath.setTextColor(getColor(android.R.color.darker_gray));
labelPath.setTextSize(14);
labelPath.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelPath);
android.widget.TextView valuePath = new android.widget.TextView(this);
valuePath.setText(imagePath);
valuePath.setTextColor(getColor(android.R.color.black));
valuePath.setTextSize(14);
valuePath.setTextIsSelectable(true);
layout.addView(valuePath);
if (imageFile.exists()) {
long sizeBytes = imageFile.length();
String size;
if (sizeBytes < 1024) {
size = sizeBytes + " B";
} else if (sizeBytes < 1024 * 1024) {
size = String.format("%.2f KB", sizeBytes / 1024.0);
} else {
size = String.format("%.2f MB", sizeBytes / (1024.0 * 1024.0));
}
android.widget.TextView labelSize = new android.widget.TextView(this);
labelSize.setText("Size:");
labelSize.setTextColor(getColor(android.R.color.darker_gray));
labelSize.setTextSize(14);
labelSize.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelSize);
android.widget.TextView valueSize = new android.widget.TextView(this);
valueSize.setText(size);
valueSize.setTextColor(getColor(android.R.color.black));
valueSize.setTextSize(14);
valueSize.setTextIsSelectable(true);
layout.addView(valueSize);
}
try {
android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
options.inJustDecodeBounds = true;
android.graphics.BitmapFactory.decodeFile(imagePath, options);
if (options.outWidth > 0 && options.outHeight > 0) {
android.widget.TextView labelPixels = new android.widget.TextView(this);
labelPixels.setText("Pixels:");
labelPixels.setTextColor(getColor(android.R.color.darker_gray));
labelPixels.setTextSize(14);
labelPixels.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelPixels);
android.widget.TextView valuePixels = new android.widget.TextView(this);
valuePixels.setText(options.outWidth + " x " + options.outHeight);
valuePixels.setTextColor(getColor(android.R.color.black));
valuePixels.setTextSize(14);
valuePixels.setTextIsSelectable(true);
layout.addView(valuePixels);
}
} catch (Exception e) {
LogUtils.e(TAG, "get pixels error: " + e.getMessage());
}
try {
String[] projection = {
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.DATE_TAKEN
};
android.database.Cursor cursor = getContentResolver().query(
imageUrls.get(currentPosition), projection, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int dateAddedCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED);
int dateTakenCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if (dateTakenCol >= 0) {
long dateTaken = cursor.getLong(dateTakenCol);
if (dateTaken > 0) {
android.widget.TextView labelTaken = new android.widget.TextView(this);
labelTaken.setText("Date Taken:");
labelTaken.setTextColor(getColor(android.R.color.darker_gray));
labelTaken.setTextSize(14);
labelTaken.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelTaken);
android.widget.TextView valueTaken = new android.widget.TextView(this);
valueTaken.setText(sdf.format(new java.util.Date(dateTaken)));
valueTaken.setTextColor(getColor(android.R.color.black));
valueTaken.setTextSize(14);
valueTaken.setTextIsSelectable(true);
layout.addView(valueTaken);
}
}
if (dateAddedCol >= 0) {
long dateAdded = cursor.getLong(dateAddedCol);
if (dateAdded > 0) {
android.widget.TextView labelAdded = new android.widget.TextView(this);
labelAdded.setText("Date Added:");
labelAdded.setTextColor(getColor(android.R.color.darker_gray));
labelAdded.setTextSize(14);
labelAdded.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelAdded);
android.widget.TextView valueAdded = new android.widget.TextView(this);
valueAdded.setText(sdf.format(new java.util.Date(dateAdded * 1000)));
valueAdded.setTextColor(getColor(android.R.color.black));
valueAdded.setTextSize(14);
valueAdded.setTextIsSelectable(true);
layout.addView(valueAdded);
}
}
}
cursor.close();
}
} catch (Exception e) {
LogUtils.e(TAG, "get date error: " + e.getMessage());
}
new AlertDialog.Builder(this)
.setTitle("Image Info")
.setView(layout)
.setPositiveButton("OK", null)
.show();
}
@Override
public void onPageSelected(int position) {
currentPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onBackPressed() {
finish();
}
}

View File

@@ -1,385 +0,0 @@
package cc.winboll.studio.gallery;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogActivity;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private static final int PERMISSION_REQUEST_CODE = 100;
private static final int MANAGE_PERMISSION_REQUEST_CODE = 101;
private RecyclerView recyclerView;
private AlbumAdapter adapter;
private Preferences prefs;
private FloatingActionButton fabScrollTop;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.d(TAG, "onCreate");
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
prefs = new Preferences(this);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
adapter = new AlbumAdapter();
adapter.setContext(this);
recyclerView.setAdapter(adapter);
fabScrollTop = findViewById(R.id.fab_scroll_top);
fabScrollTop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recyclerView.scrollToPosition(0);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int firstVisible = layoutManager.findFirstVisibleItemPosition();
if (firstVisible > 0) {
fabScrollTop.setVisibility(View.VISIBLE);
} else {
fabScrollTop.setVisibility(View.GONE);
}
}
}
});
adapter.setOnAlbumClickListener(new OnAlbumClickListener() {
@Override
public void onAlbumClick(Album album) {
Intent intent = new Intent(MainActivity.this, AlbumActivity.class);
intent.putExtra(AlbumActivity.EXTRA_ALBUM_PATH, album.getPath());
intent.putExtra(AlbumActivity.EXTRA_ALBUM_NAME, album.getName());
startActivity(intent);
}
});
checkAndRequestPermissions();
}
private void checkAndRequestPermissions() {
LogUtils.i(TAG, "checkAndRequestPermissions");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
} catch (Exception e) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
}
return;
}
}
if (checkPermission()) {
loadAlbums();
} else {
requestPermission();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MANAGE_PERMISSION_REQUEST_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
loadAlbums();
} else {
Toast.makeText(this, "Permission required", Toast.LENGTH_SHORT).show();
}
}
}
}
private boolean checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
}
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadAlbums();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
}
private void loadAlbums() {
LogUtils.d(TAG, "loadAlbums");
String folderPath = prefs.getFolderPath();
File baseFolder = new File(folderPath);
LogUtils.d(TAG, "baseFolder: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
folderPath = Preferences.getDefaultPath();
baseFolder = new File(folderPath);
LogUtils.d(TAG, "try default: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
if (!baseFolder.exists()) {
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
baseFolder = new File(folderPath);
LogUtils.d(TAG, "try Pictures: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
}
}
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
ArrayList<Album> albums = new ArrayList<>();
FileFilter directoryFilter = new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
};
File[] subfolders = baseFolder.listFiles(directoryFilter);
LogUtils.d(TAG, "subfolders: " + (subfolders != null ? subfolders.length : 0));
if (subfolders != null) {
for (File subfolder : subfolders) {
LogUtils.d(TAG, "scanning folder: " + subfolder.getName());
String albumPath = subfolder.getAbsolutePath();
String coverPath = coverDbHelper.getCover(albumPath);
LogUtils.d(TAG, "loadAlbums: album=" + albumPath + ", coverPath=" + coverPath);
Uri coverUri = null;
if (coverPath != null) {
File coverFile = new File(coverPath);
if (coverFile.exists()) {
coverUri = Uri.fromFile(coverFile);
LogUtils.d(TAG, "loadAlbums: cover from file=" + coverFile.getAbsolutePath());
} else {
coverUri = getUriFromPath(coverPath);
LogUtils.d(TAG, "loadAlbums: cover from media store path=" + coverPath);
}
}
if (coverUri == null) {
ArrayList<Uri> images = getImagesInFolder(albumPath);
if (!images.isEmpty()) {
coverUri = images.get(0);
}
}
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
if (coverUri != null || !allImages.isEmpty()) {
if (coverUri == null && !allImages.isEmpty()) {
coverUri = allImages.get(0);
}
int imageCount = allImages.size();
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + imageCount + " images");
}
}
}
if (albums.isEmpty()) {
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "No albums found");
}
adapter.setData(albums);
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
}
private Uri getUriFromPath(String path) {
String[] projection = { MediaStore.Images.Media._ID };
String selection = MediaStore.Images.Media.DATA + " = ?";
String[] selectionArgs = { path };
try (Cursor cursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, selection, selectionArgs, null)) {
if (cursor != null) {
if (cursor.moveToFirst()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
}
}
}
return null;
}
private ArrayList<Uri> getImagesInFolder(String folderPath) {
ArrayList<Uri> imageUrls = new ArrayList<>();
ContentResolver contentResolver = getContentResolver();
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String selection = MediaStore.Images.Media.DATA + " LIKE ?";
String[] selectionArgs = new String[]{folderPath + "/%"};
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
LogUtils.d(TAG, "getImagesInFolder: " + folderPath);
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
if (cursor != null) {
LogUtils.d(TAG, "cursor count: " + cursor.getCount());
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
while (cursor.moveToNext()) {
String path = cursor.getString(dataColumn);
if (path != null) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
LogUtils.d(TAG, "image: id=" + id + ", path=" + path);
imageUrls.add(contentUri);
}
}
}
}
LogUtils.d(TAG, "found " + imageUrls.size() + " images");
return imageUrls;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_mi_gallery) {
Toast.makeText(this, "Gallery clicked", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_APP_GALLERY);
startActivity(intent);
return true;
} else if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
} else if (id == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
return true;
} else if (id == R.id.action_trash) {
startActivity(new Intent(this, TrashActivity.class));
return true;
} else if (id == R.id.action_debug) {
LogActivity.startLogActivity(this);
return true;
}
return super.onOptionsItemSelected(item);
}
private BroadcastReceiver coverUpdatedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Preferences.ACTION_COVER_UPDATED.equals(intent.getAction())) {
loadAlbums();
}
}
};
@Override
protected void onResume() {
super.onResume();
registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED));
if (checkPermission()) {
scanMediaStore();
loadAlbums();
}
if (adapter != null) {
adapter.refreshBg();
adapter.refreshPinned();
adapter.refreshCover();
}
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(coverUpdatedReceiver);
}
private void scanMediaStore() {
String folderPath = prefs.getFolderPath();
File baseFolder = new File(folderPath);
if (baseFolder.exists() && baseFolder.isDirectory()) {
File[] subfolders = baseFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if (subfolders != null) {
ArrayList<String> paths = new ArrayList<>();
for (File subfolder : subfolders) {
File[] images = subfolder.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
String lower = name.toLowerCase();
return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|| lower.endsWith(".png") || lower.endsWith(".gif")
|| lower.endsWith(".webp") || lower.endsWith(".bmp");
}
});
if (images != null) {
for (File img : images) {
paths.add(img.getAbsolutePath());
}
}
}
if (!paths.isEmpty()) {
LogUtils.d(TAG, "scanning " + paths.size() + " files to MediaStore");
String[] pathArray = paths.toArray(new String[0]);
MediaScannerConnection.scanFile(this, pathArray, null, new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
}
});
}
}
}
}
}

View File

@@ -1,78 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.libappbase.LogUtils;
public class PinnedAlbumDbHelper extends SQLiteOpenHelper {
public static final String TAG = "PinnedAlbumDbHelper";
private static final String DB_NAME = "pinned_album.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "pinned_albums";
private static final String COLUMN_PATH = "album_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
private static PinnedAlbumDbHelper dbHelper;
public static PinnedAlbumDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new PinnedAlbumDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public PinnedAlbumDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public void pinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_PATH, albumPath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
LogUtils.d(TAG, "pinAlbum: " + albumPath);
}
public void unpinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "unpinAlbum: " + albumPath);
}
public boolean isPinned(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
new String[]{albumPath}, null, null, null);
boolean pinned = cursor.getCount() > 0;
cursor.close();
return pinned;
}
public String[] getPinnedPaths() {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
String[] paths = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
paths[i++] = cursor.getString(0);
}
cursor.close();
return paths;
}
}

View File

@@ -1,78 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.libappbase.LogUtils;
public class PinnedImageDbHelper extends SQLiteOpenHelper {
public static final String TAG = "PinnedImageDbHelper";
private static final String DB_NAME = "pinned_image.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "pinned_images";
private static final String COLUMN_PATH = "image_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
private static PinnedImageDbHelper dbHelper;
public static PinnedImageDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new PinnedImageDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public PinnedImageDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public void pinImage(String imagePath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_PATH, imagePath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
LogUtils.d(TAG, "pinImage: " + imagePath);
}
public void unpinImage(String imagePath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{imagePath});
LogUtils.d(TAG, "unpinImage: " + imagePath);
}
public boolean isPinned(String imagePath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
new String[]{imagePath}, null, null, null);
boolean pinned = cursor.getCount() > 0;
cursor.close();
return pinned;
}
public String[] getPinnedPaths() {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
String[] paths = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
paths[i++] = cursor.getString(0);
}
cursor.close();
return paths;
}
}

View File

@@ -1,93 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.content.SharedPreferences;
import cc.winboll.studio.libappbase.LogUtils;
public class Preferences {
public static final String TAG = "Preferences";
private static final String PREF_NAME = "gallery_prefs";
private static final String KEY_FOLDER_PATH = "folder_path";
private static final String KEY_BG_TYPE = "bg_type";
private static final String KEY_ALBUM_SORT_MODE = "album_sort_mode";
private static final String KEY_COVER_WIDTH = "cover_width";
private static final String KEY_COVER_HEIGHT = "cover_height";
private static final String KEY_COVER_RATIO = "cover_ratio";
public static final String ACTION_COVER_UPDATED = "cc.winboll.studio.gallery.COVER_UPDATED";
private static final int DEFAULT_BG_TYPE = 0;
private static final int DEFAULT_SORT_MODE = 0;
private static final int DEFAULT_COVER_WIDTH = 240;
private static final int DEFAULT_COVER_HEIGHT = 120;
private static final float DEFAULT_COVER_RATIO = 2.0f;
private static final String DEFAULT_PATH = "/storage/emulated/0/Pictures/Gallery/owner";
public static final int SORT_TIME_DESC = 0;
public static final int SORT_TIME_ASC = 1;
public static final int SORT_NAME_DESC = 2;
public static final int SORT_NAME_ASC = 3;
public static String getDefaultPath() {
return DEFAULT_PATH;
}
private final SharedPreferences prefs;
public Preferences(Context context) {
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public String getFolderPath() {
String path = prefs.getString(KEY_FOLDER_PATH, DEFAULT_PATH);
LogUtils.d(TAG, "getFolderPath: " + path);
return path;
}
public void setFolderPath(String path) {
LogUtils.d(TAG, "setFolderPath: " + path);
prefs.edit().putString(KEY_FOLDER_PATH, path).apply();
}
public int getBgType() {
return prefs.getInt(KEY_BG_TYPE, DEFAULT_BG_TYPE);
}
public void setBgType(int type) {
LogUtils.d(TAG, "setBgType: " + type);
prefs.edit().putInt(KEY_BG_TYPE, type).apply();
}
public int getAlbumSortMode() {
return prefs.getInt(KEY_ALBUM_SORT_MODE, DEFAULT_SORT_MODE);
}
public void setAlbumSortMode(int mode) {
LogUtils.d(TAG, "setAlbumSortMode: " + mode);
prefs.edit().putInt(KEY_ALBUM_SORT_MODE, mode).apply();
}
public int getCoverWidth() {
return prefs.getInt(KEY_COVER_WIDTH, DEFAULT_COVER_WIDTH);
}
public void setCoverWidth(int width) {
prefs.edit().putInt(KEY_COVER_WIDTH, width).apply();
}
public int getCoverHeight() {
return prefs.getInt(KEY_COVER_HEIGHT, DEFAULT_COVER_HEIGHT);
}
public void setCoverHeight(int height) {
prefs.edit().putInt(KEY_COVER_HEIGHT, height).apply();
}
public float getCoverRatio() {
return prefs.getFloat(KEY_COVER_RATIO, DEFAULT_COVER_RATIO);
}
public void setCoverRatio(float ratio) {
prefs.edit().putFloat(KEY_COVER_RATIO, ratio).apply();
}
}

View File

@@ -1,46 +0,0 @@
package cc.winboll.studio.gallery;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
public class SettingsActivity extends AppCompatActivity {
public static final String TAG = "SettingsActivity";
private Preferences prefs;
private EditText editFolderPath;
private TextView textCurrentPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
LogUtils.d(TAG, "onCreate");
prefs = new Preferences(this);
editFolderPath = findViewById(R.id.edit_folder_path);
textCurrentPath = findViewById(R.id.text_current_path);
Button btnSave = findViewById(R.id.btn_save);
String currentPath = prefs.getFolderPath();
editFolderPath.setText(currentPath);
textCurrentPath.setText("Current: " + currentPath);
btnSave.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String newPath = editFolderPath.getText().toString().trim();
if (!newPath.isEmpty()) {
prefs.setFolderPath(newPath);
textCurrentPath.setText("Current: " + newPath);
}
finish();
}
});
}
}

View File

@@ -1,193 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.io.File;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashActivity extends AppCompatActivity {
public static final String TAG = "TrashActivity";
private static final int PERMISSION_REQUEST_CODE = 102;
private RecyclerView recyclerView;
private TrashAdapter adapter;
private TrashManager trashManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.d(TAG, "onCreate");
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle("Trash");
trashManager = new TrashManager(this);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
adapter = new TrashAdapter();
recyclerView.setAdapter(adapter);
adapter.setOnTrashClickListener(new TrashAdapter.OnTrashClickListener() {
@Override
public void onRestoreClick(int position) {
restoreImage(position);
}
@Override
public void onDeleteClick(int position) {
permanentlyDelete(position);
}
});
if (checkPermission()) {
loadTrash();
} else {
requestPermission();
}
}
private boolean checkPermission() {
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadTrash();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
}
}
}
private void loadTrash() {
LogUtils.d(TAG, "loadTrash");
Cursor cursor = trashManager.getTrashList();
ArrayList<TrashItem> items = new ArrayList<TrashItem>();
ArrayList<Uri> uris = new ArrayList<Uri>();
String trashPath = TrashDbHelper.getTrashPath();
File trashDir = new File(trashPath);
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
do {
try {
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
String originalPath = cursor.getString(cursor.getColumnIndexOrThrow("original_path"));
String originalFolder = cursor.getString(cursor.getColumnIndexOrThrow("original_folder"));
TrashItem item = new TrashItem();
item.id = id;
item.fileName = fileName;
item.originalPath = originalPath;
item.originalFolder = originalFolder;
File trashFile = new File(trashDir, fileName);
if (trashFile.exists()) {
items.add(item);
uris.add(Uri.fromFile(trashFile));
}
} catch (Exception e) {
e.printStackTrace();
}
} while (cursor.moveToNext());
cursor.close();
}
adapter.setData(items, uris);
if (items.isEmpty()) {
Toast.makeText(this, "Trash is empty", Toast.LENGTH_SHORT).show();
}
}
private void restoreImage(int position) {
LogUtils.d(TAG, "restoreImage: " + position);
long id = adapter.getItemId(position);
String fileName = adapter.getFileName(position);
String originalPath = adapter.getOriginalPath(position);
if (trashManager.restore(id, fileName, originalPath)) {
Toast.makeText(this, "Image restored", Toast.LENGTH_SHORT).show();
LogUtils.i(TAG, "Image restored");
loadTrash();
} else {
Toast.makeText(this, "Restore failed", Toast.LENGTH_SHORT).show();
}
}
private void permanentlyDelete(int position) {
long id = adapter.getItemId(position);
String fileName = adapter.getFileName(position);
if (trashManager.deletePermanently(id, fileName)) {
Toast.makeText(this, "Image deleted", Toast.LENGTH_SHORT).show();
loadTrash();
} else {
Toast.makeText(this, "Delete failed", Toast.LENGTH_SHORT).show();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_trash, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_clear_trash) {
trashManager.clearTrash();
Toast.makeText(this, "Trash cleared", Toast.LENGTH_SHORT).show();
loadTrash();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
if (checkPermission()) {
loadTrash();
}
}
public static class TrashItem {
public long id;
public String fileName;
public String originalPath;
public String originalFolder;
}
}

View File

@@ -1,111 +0,0 @@
package cc.winboll.studio.gallery;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashAdapter extends RecyclerView.Adapter<TrashAdapter.ViewHolder> {
public static final String TAG = "TrashAdapter";
private ArrayList<TrashActivity.TrashItem> trashItems = new ArrayList<TrashActivity.TrashItem>();
private ArrayList<Uri> imageUrls = new ArrayList<Uri>();
private OnTrashClickListener listener;
public interface OnTrashClickListener {
void onRestoreClick(int position);
void onDeleteClick(int position);
}
public void setOnTrashClickListener(OnTrashClickListener listener) {
this.listener = listener;
}
public void setData(ArrayList<TrashActivity.TrashItem> items, ArrayList<Uri> uris) {
this.trashItems = items;
this.imageUrls = uris;
LogUtils.d(TAG, "setData: " + items.size() + " items");
notifyDataSetChanged();
}
public long getItemId(int position) {
if (position >= 0 && position < trashItems.size()) {
return trashItems.get(position).id;
}
return -1;
}
public String getFileName(int position) {
if (position >= 0 && position < trashItems.size()) {
return trashItems.get(position).fileName;
}
return "";
}
public String getOriginalPath(int position) {
if (position >= 0 && position < trashItems.size()) {
return trashItems.get(position).originalPath;
}
return "";
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_trash, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
if (position < imageUrls.size()) {
Glide.with(holder.imageView.getContext())
.load(imageUrls.get(position))
.centerCrop()
.into(holder.imageView);
}
holder.btnRestore.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onRestoreClick(position);
}
}
});
holder.btnDelete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onDeleteClick(position);
}
}
});
}
@Override
public int getItemCount() {
return trashItems.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
ImageView btnRestore;
ImageView btnDelete;
ViewHolder(View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image);
btnRestore = itemView.findViewById(R.id.btn_restore);
btnDelete = itemView.findViewById(R.id.btn_delete);
}
}
}

View File

@@ -1,78 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
import java.io.File;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashDbHelper extends SQLiteOpenHelper {
public static final String TAG = "TrashDbHelper";
private static final String DB_NAME = "trash.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "trash_items";
private static final String COL_ID = "_id";
private static final String COL_FILE_NAME = "file_name";
private static final String COL_ORIGINAL_PATH = "original_path";
private static final String COL_ORIGINAL_FOLDER = "original_folder";
private static final String COL_DELETE_TIME = "delete_time";
public TrashDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
LogUtils.d(TAG, "onCreate");
db.execSQL("CREATE TABLE " + TABLE_NAME + " (" +
COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COL_FILE_NAME + " TEXT, " +
COL_ORIGINAL_PATH + " TEXT, " +
COL_ORIGINAL_FOLDER + " TEXT, " +
COL_DELETE_TIME + " INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LogUtils.i(TAG, "onUpgrade: " + oldVersion + " -> " + newVersion);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public long insert(String fileName, String originalPath, String originalFolder) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COL_FILE_NAME, fileName);
values.put(COL_ORIGINAL_PATH, originalPath);
values.put(COL_ORIGINAL_FOLDER, originalFolder);
values.put(COL_DELETE_TIME, System.currentTimeMillis());
return db.insert(TABLE_NAME, null, values);
}
public Cursor getAll() {
SQLiteDatabase db = getReadableDatabase();
return db.query(TABLE_NAME, null, null, null, null, null, COL_DELETE_TIME + " DESC");
}
public int delete(long id) {
SQLiteDatabase db = getWritableDatabase();
return db.delete(TABLE_NAME, COL_ID + "=?", new String[]{String.valueOf(id)});
}
public void clear() {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
public static String getTrashPath() {
File trashDir = new File(Environment.getExternalStorageDirectory(), ".Trash");
if (!trashDir.exists()) {
trashDir.mkdirs();
}
return trashDir.getAbsolutePath();
}
}

View File

@@ -1,158 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import java.io.File;
import java.util.UUID;
import cc.winboll.studio.libappbase.LogUtils;
public class TrashManager {
public static final String TAG = "TrashManager";
private final Context context;
private final TrashDbHelper dbHelper;
public TrashManager(Context context) {
LogUtils.d(TAG, "TrashManager created");
this.context = context;
this.dbHelper = new TrashDbHelper(context);
}
public long addToTrash(String imagePath) {
LogUtils.d(TAG, "addToTrash: " + imagePath);
File sourceFile = new File(imagePath);
if (!sourceFile.exists()) {
return -1;
}
String uniqueId = UUID.randomUUID().toString();
String extension = getExtension(imagePath);
String newFileName = uniqueId + extension;
String trashPath = TrashDbHelper.getTrashPath();
File destFile = new File(trashPath, newFileName);
if (sourceFile.renameTo(destFile)) {
String originalFolder = sourceFile.getParent();
long result = dbHelper.insert(newFileName, imagePath, originalFolder);
LogUtils.i(TAG, "Added to trash: " + newFileName);
return result;
}
LogUtils.e(TAG, "Failed to move to trash");
return -1;
}
public Cursor getTrashList() {
return dbHelper.getAll();
}
public boolean restore(long id, String fileName, String originalPath) {
LogUtils.i(TAG, "restore: " + fileName + " -> " + originalPath);
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
LogUtils.d(TAG, "trashFile exists: " + trashFile.exists() + ", path: " + trashFile.getAbsolutePath());
if (!trashFile.exists()) {
LogUtils.e(TAG, "trashFile not exists: " + trashFile.getAbsolutePath());
return false;
}
File originalFolder = new File(originalPath).getParentFile();
LogUtils.d(TAG, "originalFolder: " + originalFolder + ", exists: " + (originalFolder != null && originalFolder.exists()));
if (originalFolder != null && !originalFolder.exists()) {
boolean created = originalFolder.mkdirs();
LogUtils.d(TAG, "mkdirs result: " + created + ", path: " + originalFolder.getAbsolutePath());
}
File originalFile = new File(originalPath);
String restoreName = originalFile.getName();
File restoreFile = new File(originalFolder, restoreName);
LogUtils.d(TAG, "restoreFile: " + restoreFile.getAbsolutePath() + ", exists: " + restoreFile.exists());
boolean renameResult = trashFile.renameTo(restoreFile);
LogUtils.d(TAG, "renameTo result: " + renameResult);
if (renameResult) {
dbHelper.delete(id);
LogUtils.i(TAG, "Restored: " + fileName);
scanMedia(restoreFile.getAbsolutePath());
return true;
}
// Try copy + delete if rename failed
LogUtils.i(TAG, "renameTo failed, trying copy + delete");
try {
java.io.InputStream in = new java.io.FileInputStream(trashFile);
java.io.OutputStream out = new java.io.FileOutputStream(restoreFile);
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
in.close();
out.close();
boolean deleted = trashFile.delete();
LogUtils.d(TAG, "copy+delete result: " + deleted);
if (deleted) {
dbHelper.delete(id);
LogUtils.i(TAG, "Restored (copy): " + fileName);
scanMedia(restoreFile.getAbsolutePath());
return true;
}
} catch (Exception e) {
LogUtils.e(TAG, "copy failed: " + e.getMessage());
}
LogUtils.e(TAG, "Failed to restore: " + fileName);
return false;
}
public boolean deletePermanently(long id, String fileName) {
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
boolean deleted = trashFile.delete();
if (deleted) {
dbHelper.delete(id);
}
return deleted;
}
public void clearTrash() {
Cursor cursor = getTrashList();
if (cursor != null) {
while (cursor.moveToNext()) {
try {
int colIndex = cursor.getColumnIndexOrThrow("_id");
if (!cursor.isNull(colIndex)) {
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
trashFile.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
cursor.close();
}
dbHelper.clear();
}
private String getExtension(String path) {
int lastDot = path.lastIndexOf('.');
if (lastDot > 0) {
return path.substring(lastDot);
}
return ".jpg";
}
private void scanMedia(String filePath) {
LogUtils.d(TAG, "scanMedia: " + filePath);
MediaScannerConnection.scanFile(context, new String[]{filePath}, null, new android.media.MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
}
});
}
}

View File

@@ -1,169 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.FrameLayout;
public class ZoomContainerView extends FrameLayout {
private float scaleFactor = 1.0f;
private float minScale = 0.1f;
private float maxScale = 5.0f;
private static final float ZOOM_STEP = 0.25f;
private ScaleGestureDetector scaleGestureDetector;
private GestureDetector gestureDetector;
private Paint borderPaint;
public ZoomContainerView(Context context) {
super(context);
init();
}
public ZoomContainerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ZoomContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setWillNotDraw(false);
setBackgroundColor(Color.YELLOW);
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#333333"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(2);
scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(minScale, Math.min(scaleFactor, maxScale));
invalidate();
requestLayout();
return true;
}
});
gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (scaleFactor > 1.5f) {
scaleFactor = 1.0f;
} else {
scaleFactor = 2.0f;
}
invalidate();
requestLayout();
return true;
}
});
}
public void zoomIn() {
scaleFactor = Math.min(maxScale, scaleFactor + ZOOM_STEP);
requestMeasure();
}
public void zoomOut() {
scaleFactor = Math.max(minScale, scaleFactor - ZOOM_STEP);
requestMeasure();
}
private void requestMeasure() {
removeCallbacks(measureRunable);
post(measureRunable);
}
private Runnable measureRunable = new Runnable() {
@Override
public void run() {
requestLayout();
invalidate();
}
};
public float getScaleFactor() {
return scaleFactor;
}
public void setScaleFactor(float scale) {
scaleFactor = Math.max(minScale, Math.min(scale, maxScale));
int childCount = getChildCount();
if (childCount > 0) {
View child = getChildAt(0);
if (child instanceof CropCanvasView) {
((CropCanvasView) child).setContainerScale(scaleFactor);
}
}
requestLayout();
invalidate();
}
public void resetZoom() {
scaleFactor = 1.0f;
invalidate();
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
if (childCount > 0) {
View child = getChildAt(0);
child.measure(widthMeasureSpec, heightMeasureSpec);
int childW = child.getMeasuredWidth();
int childH = child.getMeasuredHeight();
int scaledW = (int) (childW * scaleFactor);
int scaledH = (int) (childH * scaleFactor);
int widthSpec = MeasureSpec.makeMeasureSpec(scaledW, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(scaledH, MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
setMeasuredDimension(scaledW, scaledH);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childCount = getChildCount();
if (childCount > 0) {
View child = getChildAt(0);
int childW = child.getMeasuredWidth();
int childH = child.getMeasuredHeight();
int scaledW = (int) (childW * scaleFactor);
int scaledH = (int) (childH * scaleFactor);
int parentW = right - left;
int parentH = bottom - top;
int childLeft = (parentW - scaledW) / 2;
int childTop = (parentH - scaledH) / 2;
child.layout(childLeft, childTop, childLeft + scaledW, childTop + scaledH);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = scaleGestureDetector.onTouchEvent(event);
if (!scaleGestureDetector.isInProgress()) {
handled = gestureDetector.onTouchEvent(event) || handled;
}
return handled || super.onTouchEvent(event);
}
}

View File

@@ -1,44 +0,0 @@
package cc.winboll.studio.gallery.dialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
import cc.winboll.studio.gallery.R;
/**
* 颜色表对话框
* 继承于普通对话框类,使用视图文件
*/
public class ColorPaletteDialog extends Dialog {
public ColorPaletteDialog(@NonNull Context context) {
super(context, R.style.ColorPaletteDialog);
}
public ColorPaletteDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_color_palette);
TextView titleText = findViewById(R.id.title_text);
WindowManager.LayoutParams params = getWindow().getAttributes();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
getWindow().setAttributes(params);
}
public void setTitle(String title) {
}
public interface OnColorItemClick {
void onColorClick(int color);
}
}

View File

@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10"
android:viewportHeight="10">
<path
android:fillColor="#808080"
android:pathData="M0,0h1v1h-1z M2,0h1v1h-1z M4,0h1v1h-1z M6,0h1v1h-1z M8,0h1v1h-1z
M1,1h1v1h-1z M3,1h1v1h-1z M5,1h1v1h-1z M7,1h1v1h-1z M9,1h1v1h-1z
M0,2h1v1h-1z M2,2h1v1h-1z M4,2h1v1h-1z M6,2h1v1h-1z M8,2h1v1h-1z
M1,3h1v1h-1z M3,3h1v1h-1z M5,3h1v1h-1z M7,3h1v1h-1z M9,3h1v1h-1z
M0,4h1v1h-1z M2,4h1v1h-1z M4,4h1v1h-1z M6,4h1v1h-1z M8,4h1v1h-1z
M1,5h1v1h-1z M3,5h1v1h-1z M5,5h1v1h-1z M7,5h1v1h-1z M9,5h1v1h-1z
M0,6h1v1h-1z M2,6h1v1h-1z M4,6h1v1h-1z M6,6h1v1h-1z M8,6h1v1h-1z
M1,7h1v1h-1z M3,7h1v1h-1z M5,7h1v1h-1z M7,7h1v1h-1z M9,7h1v1h-1z
M0,8h1v1h-1z M2,8h1v1h-1z M4,8h1v1h-1z M6,8h1v1h-1z M8,8h1v1h-1z
M1,9h1v1h-1z M3,9h1v1h-1z M5,9h1v1h-1z M7,9h1v1h-1z M9,9h1v1h-1z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M1,0h1v1h-1z M3,0h1v1h-1z M5,0h1v1h-1z M7,0h1v1h-1z M9,0h1v1h-1z
M0,1h1v1h-1z M2,1h1v1h-1z M4,1h1v1h-1z M6,1h1v1h-1z M8,1h1v1h-1z
M1,2h1v1h-1z M3,2h1v1h-1z M5,2h1v1h-1z M7,2h1v1h-1z M9,2h1v1h-1z
M0,3h1v1h-1z M2,3h1v1h-1z M4,3h1v1h-1z M6,3h1v1h-1z M8,3h1v1h-1z
M1,4h1v1h-1z M3,4h1v1h-1z M5,4h1v1h-1z M7,4h1v1h-1z M9,4h1v1h-1z
M0,5h1v1h-1z M2,5h1v1h-1z M4,5h1v1h-1z M6,5h1v1h-1z M8,5h1v1h-1z
M1,6h1v1h-1z M3,6h1v1h-1z M5,6h1v1h-1z M7,6h1v1h-1z M9,6h1v1h-1z
M0,7h1v1h-1z M2,7h1v1h-1z M4,7h1v1h-1z M6,7h1v1h-1z M8,7h1v1h-1z
M1,8h1v1h-1z M3,8h1v1h-1z M5,8h1v1h-1z M7,8h1v1h-1z M9,8h1v1h-1z
M0,9h1v1h-1z M2,9h1v1h-1z M4,9h1v1h-1z M6,9h1v1h-1z M8,9h1v1h-1z"/>
</vector>

View File

@@ -1,7 +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="@android:color/transparent"/>
<stroke android:width="2dp" android:color="@android:color/white"/>
<size android:width="32dp" android:height="32dp"/>
</shape>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke android:width="2dp" android:color="@android:color/white"/>
<size android:width="36dp" android:height="36dp"/>
</shape>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/black"/>
<stroke android:width="2dp" android:color="@android:color/white"/>
</shape>

View File

@@ -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="#FFFFFF"
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@@ -1,12 +0,0 @@
<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="#FFFFFF"
android:pathData="M4,4h7v7h-7z M13,4h7v7h-7z M4,13h7v7h-7z M13,13h7v7h-7z"/>
<path
android:fillColor="#808080"
android:pathData="M11,4h2v7h-2z M4,11h7v2h-7z M13,11h7v2h-7z M11,13h2v7h-2z M13,4h2v7h-2z M4,13h7v2h-7z"/>
</vector>

View File

@@ -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="#FFFFFF"
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>

View File

@@ -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="#FFFFFF"
android:pathData="M20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.12,3.12 -1.93,-1.91 -1.41,1.41 1.42,1.42L3,16.25V21h4.75l8.92,-8.92 1.42,1.42 1.41,-1.41 -1.92,-1.92 3.12,-3.12c0.4,-0.4 0.4,-1.03 0.01,-1.42zM6.92,19H5v-1.92l8.06,-8.06 1.92,1.92L6.92,19z"/>
</vector>

View File

@@ -1,13 +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="#FFFFFF"
android:pathData="M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3zM21,19H3V5h18V19z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M9,12l2,2l4,-4l1.5,1.5L11,17l-3,-3z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
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>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
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>

View File

@@ -1,16 +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="#000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8s-3.59,8 -8,8z"/>
<path
android:fillColor="#000000"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5s-2.24,-5 -5,-5zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3s-1.34,3 -3,3z"/>
<path
android:fillColor="#000000"
android:pathData="M8,12l1.5,-2l2,2.5l2.5,-3l2,2.5"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.5V22H12.5V16H18V14L16,12Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89 0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,17.9 10.51,19 13,19c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08V8H12z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92 -1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -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="#FFFFFF"
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zM3,13h12v-2H3v2z"/>
</vector>

View File

@@ -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:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M1,3V21H23V3H1M21,5V14H3V5H21M11,16V19H8V16H11M3,16H6V19H3V16M13,19V16H16V19H13M18,19V16H21V19H18Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<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="#FFFFFF"
android:pathData="M19,13H5v-2h14v2z"/>
</vector>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/aboutview"/>
</LinearLayout>
</FrameLayout>

View File

@@ -1,143 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/colorPrimary"
android:paddingHorizontal="16dp">
<ImageView
android:id="@+id/btn_close"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="5dp"
android:src="@drawable/ic_close"
android:background="?attr/selectableItemBackgroundBorderless"/>
<TextView
android:id="@+id/textview_window_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="封面剪裁"
android:textColor="@android:color/white"
android:textSize="18sp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:gravity="center"
android:layout_marginEnd="8dp"/>
<FrameLayout
android:id="@+id/color_icon_container"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:background="@android:color/transparent">
<View
android:id="@+id/color_view"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:background="@drawable/bg_color_circle"/>
</FrameLayout>
<ImageView
android:id="@+id/btn_color_pick"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="5dp"
android:src="@drawable/ic_color_pick"
android:background="?attr/selectableItemBackgroundBorderless"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<ImageView
android:id="@+id/btn_info"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="5dp"
android:src="@drawable/ic_info"
android:background="?attr/selectableItemBackgroundBorderless"/>
<ImageView
android:id="@+id/btn_done"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="5dp"
android:src="@drawable/ic_done"
android:background="?attr/selectableItemBackgroundBorderless"/>
</LinearLayout>
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="56dp"
android:layout_marginBottom="56dp"
android:fillViewport="true">
<cc.winboll.studio.gallery.ZoomContainerView
android:id="@+id/zoom_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center">
<cc.winboll.studio.gallery.CropCanvasView
android:id="@+id/crop_canvas_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</cc.winboll.studio.gallery.ZoomContainerView>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/colorPrimary"
android:paddingHorizontal="16dp"
android:layout_marginBottom="0dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="缩小"
android:textColor="@android:color/white"
android:textSize="12sp"/>
<SeekBar
android:id="@+id/seekbar_zoom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:min="0"
android:max="100"
android:progress="50"
android:layout_marginHorizontal="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="放大"
android:textColor="@android:color/white"
android:textSize="12sp"/>
</LinearLayout>
</FrameLayout>

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:clickable="true"
android:focusable="true">
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"/>
<LinearLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="#CC000000"
android:orientation="horizontal"
android:padding="4dp">
<ImageButton
android:id="@+id/btn_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_back"
android:contentDescription="Back"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<ImageButton
android:id="@+id/btn_gallery"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_view_gallery_outline"
android:contentDescription="Gallery"/>
<ImageButton
android:id="@+id/btn_share"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_share"
android:contentDescription="Share"/>
<ImageButton
android:id="@+id/btn_info"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_info"
android:contentDescription="Info"/>
<ImageButton
android:id="@+id/btn_bg"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_bg"
android:contentDescription="Background"/>
<ImageButton
android:id="@+id/btn_delete"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_delete"
android:contentDescription="Delete"/>
</LinearLayout>
</FrameLayout>

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"/>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_scroll_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_arrow_up"
android:visibility="gone"
app:backgroundTint="@color/colorAccent"
app:tint="@color/white"/>
</FrameLayout>

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/folder_path"
android:textSize="16sp"
android:textStyle="bold"/>
<EditText
android:id="@+id/edit_folder_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/enter_folder_path"
android:inputType="text"/>
<TextView
android:id="@+id/text_current_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:textColor="#888888"/>
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/save"/>
</LinearLayout>

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="选择背景"
android:textSize="20sp"
android:textStyle="bold"
android:gravity="center"
android:paddingBottom="16dp" />
<RadioGroup
android:id="@+id/radio_group_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/radio_checkerboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="灰白相间"
android:padding="12dp" />
<RadioButton
android:id="@+id/radio_white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="全白"
android:padding="12dp" />
<RadioButton
android:id="@+id/radio_black"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="全黑"
android:padding="12dp" />
</RadioGroup>
</LinearLayout>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_dialog"
android:padding="16dp">
<TextView
android:id="@+id/title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/white"
android:textStyle="bold"/>
</FrameLayout>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/bg_dialog">
<TextView
android:id="@+id/info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:fontFamily="monospace"
android:textColor="@android:color/white"/>
<ImageView
android:id="@+id/preview_image"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="16dp"
android:scaleType="fitCenter"
android:background="@android:color/black"/>
</LinearLayout>

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp">
<ImageView
android:id="@+id/album_cover"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="2dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/album_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#80000000"
android:padding="4dp"
android:textColor="@color/white"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"/>
<ImageView
android:id="@+id/pin_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="bottom|end"
android:layout_margin="6dp"
android:background="@android:color/transparent"
android:padding="3dp"
android:src="@drawable/ic_pin"
android:visibility="gone"/>
<TextView
android:id="@+id/image_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:background="#80000000"
android:padding="4dp"
android:textColor="@color/white"
android:textSize="10sp"/>
</FrameLayout>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="40dp"
android:layout_height="40dp">
<View
android:id="@+id/color_view"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:background="@drawable/bg_color_circle"/>
<View
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:background="@drawable/bg_color_circle_border"/>
</FrameLayout>

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="120dp"
android:scaleType="centerCrop"
android:background="@color/black"/>
<ImageView
android:id="@+id/pin_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|end"
android:layout_margin="4dp"
android:background="@drawable/bg_circle_white"
android:padding="4dp"
android:src="@drawable/ic_pin"
android:visibility="gone"
app:tint="@color/black"
xmlns:app="http://schemas.android.com/apk/res-auto"/>
<ImageView
android:id="@+id/cover_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="bottom|end"
android:layout_margin="4dp"
android:background="@drawable/bg_circle_white"
android:padding="4dp"
android:src="@drawable/ic_cover"
android:visibility="gone"
app:tint="@color/black"
xmlns:app="http://schemas.android.com/apk/res-auto"/>
</FrameLayout>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"/>
</FrameLayout>

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="120dp"
android:scaleType="centerCrop"
android:background="@color/black"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#80000000"
android:orientation="horizontal">
<ImageView
android:id="@+id/btn_restore"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:padding="8dp"
android:src="@drawable/ic_restore"
android:contentDescription="Restore"/>
<ImageView
android:id="@+id/btn_delete"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:padding="8dp"
android:src="@drawable/ic_delete"
android:contentDescription="Delete"/>
</LinearLayout>
</FrameLayout>

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_sort"
android:icon="@drawable/ic_sort"
android:title="排序"
app:showAsAction="ifRoom">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/sort_time_desc"
android:title="时间倒序"/>
<item
android:id="@+id/sort_time_asc"
android:title="时间正序"/>
<item
android:id="@+id/sort_name_desc"
android:title="名称倒序"/>
<item
android:id="@+id/sort_name_asc"
android:title="名称正序"/>
</group>
</menu>
</item>
</menu>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_done"
android:title="完成"
app:showAsAction="always"/>
</menu>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_mi_gallery"
android:title="@string/mi_gallery"
android:icon="@drawable/ic_mi_gallery"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_trash"
android:title="@string/trash"
app:showAsAction="never"/>
<item
android:id="@+id/action_settings"
android:title="@string/settings"
app:showAsAction="never"/>
<item
android:id="@+id/action_about"
android:title="@string/about"
app:showAsAction="never"/>
<item
android:id="@+id/action_debug"
android:title="@string/debug_log"
app:showAsAction="never"/>
</menu>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_trash"
android:title="@string/trash"
app:showAsAction="never"/>
<item
android:id="@+id/action_clear_trash"
android:title="@string/clear_trash"
app:showAsAction="never"/>
</menu>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#009688</color>
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#FF9800</color>
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
</resources>

View File

@@ -1,24 +0,0 @@
<resources>
<string name="app_name">Gallery</string>
<string name="app_description">WinBoLL Album App</string>
<string name="refresh">Refresh</string>
<string name="settings">Settings</string>
<string name="settings_title">Settings</string>
<string name="about">About</string>
<string name="folder_path">Folder Path</string>
<string name="enter_folder_path">Enter folder path</string>
<string name="save">Save</string>
<string name="cancel">Cancel</string>
<string name="no_images_found">No images found</string>
<string name="trash">Trash</string>
<string name="clear_trash">Clear Trash</string>
<string name="delete_confirm">Delete to trash?</string>
<string name="restore_confirm">Restore to original folder?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="debug_log">Debug Log</string>
<string name="debug_message">Debug log message</string>
<string name="system_gallery">系统相册</string>
<string name="mi_gallery">小米相册</string>
<string name="reset_gallery">重置</string>
</resources>

View File

@@ -1,16 +0,0 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="ColorPaletteDialog" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">false</item>
</style>
</resources>

0
gradlew vendored Executable file → Normal file
View File

1
powerbell/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

112
powerbell/README.md Normal file
View File

@@ -0,0 +1,112 @@
# PowerBell
#### 介绍
一个接收手机电量信息的应用,当电量值达到设定范围时会提醒用户。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 gradle assembleStageRelease
#### 使用说明
在安卓系统中需要设置两个权限允许。
1.自启动权限允许。
2.省电策略-无限制权限允许。
3.设置背景图片需要读写手机存储权限。
4.要在锁屏充电的时候提醒,还需要设置允许锁屏通知权限。
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@qq.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档
AndroidManifest.xml详解
https://www.jianshu.com/p/3b5b89d4e154
CrashHandler自定义异常处理
https://www.jianshu.com/p/9a3d800a429a
Android用Intent启动Activity的方法
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
Android开发最详细的 Toolbar 开发实践总结
https://www.jianshu.com/p/79604c3ddcae
Using the App Toolbar
https://guides.codepath.com/android/using-the-app-toolbar
Android的onCreateOptionsMenu()创建菜单Menu详解
https://www.cnblogs.com/spring87/p/4312538.html
Android通知栏-Notification通知消息
https://blog.csdn.net/qq_35507234/article/details/90676587
android之PendingIntent的使用
https://blog.csdn.net/qq_16628781/article/details/51548324
change seekbar color android” Code Answer
https://www.codegrepper.com/code-examples/whatever/change+seekbar+color+android
如何选择开源项目许可证
https://www.zhihu.com/question/28292322
Android最简单的自定义布局Notification
https://blog.csdn.net/acesheep_911/article/details/81458784?utm_medium=distribute.wap_relevant.none-task-blog-2~default~baidujs_title~default-0.wap_blog_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3
Android中通知栏Notification详解以及自定义Notification
https://blog.csdn.net/daitu_liang/article/details/50246803
Android 图像系列: 将本地图片加载到Drawable
https://blog.csdn.net/qzone123222/article/details/7930035
android 从相册选择,Android开发从相册中选取照片
https://blog.csdn.net/weixin_42146086/article/details/117570917
Android 任务栈简介
https://blog.csdn.net/qq_34368586/article/details/107653912
Android用Intent启动Activity的方法
https://blog.csdn.net/huangxiaohu_coder/article/details/7105457
Android中使用dimen定义尺寸
https://blog.csdn.net/yuzhiboyi/article/details/7696174
declare-styleable自定义控件的属性
https://blog.csdn.net/congqingbin/article/details/7869730
安卓自定义滑动解锁控件
https://blog.csdn.net/lp506954774/article/details/72677018
Android 添加菜单和返回按钮
https://blog.csdn.net/my_tiantian/article/details/77822173
Android Button的基本使用
https://www.cnblogs.com/yishaochu/p/5783605.html
Android应用中实现系统“分享”接口
https://blog.csdn.net/lowprofile_coding/article/details/37656255
Android 关于mimeType的使用
https://blog.csdn.net/dorytmx/article/details/80972248
使用GitHub Actions实现Android自动打包apk
https://blog.csdn.net/ZZL23333/article/details/115798615?app_version=6.0.0&code=app_1562916241&csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22115798615%22%2C%22source%22%3A%22weixin_38986226%22%7D&uLinkId=usr1mkqgl919blen&utm_source=app

View File

@@ -18,20 +18,19 @@ def genVersionName(def versionName){
}
android {
// MIUI12
// MIUI12
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.gallery"
minSdkVersion 23
// MIUI12
applicationId "cc.winboll.studio.powerbell"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionCode 7
// versionName
// .winboll/winbollBuildProps.properties stageCount=0
// Gradle编译环境下合起来的 versionName "${versionName}.0"
versionName "15.0"
versionName "15.15"
if(true) {
versionName = genVersionName("${versionName}")
}
@@ -41,21 +40,27 @@ android {
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
sourceSets {
main {
jniLibs.srcDirs = ['libs'] // SO库放在libs目录下
}
}
}
dependencies {
api 'com.google.code.gson:gson:2.10.1'
//
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
//
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//使sdk
//5
//api 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// uCrop
implementation 'com.github.yalantis:ucrop:2.2.8'
// AndroidXAndroidX
//implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
//
api 'io.github.medyo:android-about-page:2.0.0'
// SSH
api 'com.jcraft:jsch:0.1.55'
// Html
@@ -63,57 +68,25 @@ dependencies {
//
api 'com.google.zxing:core:3.4.1'
api 'com.journeyapps:zxing-android-embedded:3.6.0'
//
api 'io.github.medyo:android-about-page:2.0.0'
//
//
api 'com.squareup.okhttp3:okhttp:4.4.1'
// OkHttp网络请求
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
// FastJSON解析
implementation 'com.alibaba:fastjson:1.2.76'
// AndroidX
/*api 'androidx.appcompat:appcompat:1.1.0'
//api 'com.google.android.material:material:1.4.0'
// AndroidX
api 'androidx.appcompat:appcompat:1.1.0'
api 'com.google.android.material:material:1.4.0'
//api 'androidx.viewpager:viewpager:1.0.0'
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'*/
//
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//使sdk
//5
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.guava:guava:24.1-jre"
/*
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
*/
/*implementation 'com.termux:terminal-emulator:0.118.0'
implementation 'com.termux:terminal-view:0.118.0'
implementation 'com.termux:termux-shared:0.118.0'
*/
//api 'androidx.fragment:fragment:1.1.0'
// WinBoLL库 nexus.winboll.cc
api 'cc.winboll.studio:libaes:15.15.9'
api 'cc.winboll.studio:libappbase:15.15.21'
api 'cc.winboll.studio:libaes:15.15.2'
api 'cc.winboll.studio:libappbase:15.15.11'
// WinBoLL备用库 jitpack.io
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
//api 'com.github.ZhanGSKen:AES:aes-v15.15.2'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
api fileTree(dir: 'libs', include: ['*.jar'])
//api fileTree(dir: 'libs', include: ['*.aar'])
api fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Feb 04 11:07:22 HKT 2026
stageCount=9
libraryProject=
baseVersion=15.15
publishVersion=15.15.8
buildCount=0
baseBetaVersion=15.15.9

View File

@@ -0,0 +1,279 @@
#!/bin/bash
# PowerBell软著版本号快速修改+生成脚本
# 无需手动改主脚本,输入版本号直接运行
# 颜色输出函数
red_echo() { echo -e "\033[31m$1\033[0m"; }
green_echo() { echo -e "\033[32m$1\033[0m"; }
blue_echo() { echo -e "\033[34m$1\033[0m"; }
# 1. 提示用户输入新版本号
blue_echo "==== 请输入软著版本号格式示例V15、V15.0.1 ===="
read -p "输入版本号:" NEW_VERSION
# 校验版本号格式(避免特殊符号)
if [[ ! $NEW_VERSION =~ ^V[0-9]+(\.[0-9]+)*$ ]]; then
red_echo "错误版本号格式无效请遵循「V+数字」格式如V15、V15.0.1),不含特殊符号"
exit 1
fi
# 2. 定义固定配置(仅需修改这里的著作权人,其他无需动)
SOFTWARE_NAME="PowerBell"
COPYRIGHT_OWNER="张绍建陆丰东海镇云宝软件开发工作室"
LINES_PER_PAGE=55
# 3. 生成主脚本(自动替换新版本号)
blue_echo -e "\n==== 生成${NEW_VERSION}版本主脚本 ===="
cat > build_copyright_pdf_temp.sh << EOF
#!/bin/bash
# PowerBell软著PDF生成脚本版本$NEW_VERSION
red_echo() { echo -e "\033[31m\$1\033[0m"; }
green_echo() { echo -e "\033[32m\$1\033[0m"; }
blue_echo() { echo -e "\033[34m\$1\033[0m"; }
# 配置项(已自动替换为${NEW_VERSION}
SOFTWARE_NAME="$SOFTWARE_NAME"
SOFTWARE_VERSION="$NEW_VERSION"
COPYRIGHT_OWNER="$COPYRIGHT_OWNER"
LINES_PER_PAGE=$LINES_PER_PAGE
# 步骤1检查依赖
blue_echo "==== 1/7 检查并安装依赖 ===="
sudo apt update > /dev/null 2>&1
REQUIRED_PKGS=("python3" "wkhtmltopdf" "fonts-wqy-microhei" "pdftk" "poppler-utils")
for pkg in "\${REQUIRED_PKGS[@]}"; do
if ! dpkg -s "\$pkg" > /dev/null 2>&1; then
green_echo "安装依赖:\$pkg"
sudo apt install -y "\$pkg" > /dev/null 2>&1
fi
done
# 步骤2生成纯文本源码
blue_echo -e "\n==== 2/7 生成纯文本核心源码 ===="
cat > generate_source.py << GEN_EOF
import os
PROJECT_PATH = "./"
OUTPUT_TXT = "PowerBell_Core_Source.txt"
INCLUDE_EXT = [".java", ".kt"]
EXCLUDE_DIRS = ["build", "libs", "test", "androidTest", ".git", ".idea", "gradle", "unittest"]
MIN_LINE_COUNT = 3
SOFTWARE_NAME = "$SOFTWARE_NAME"
SOFTWARE_VERSION = "$NEW_VERSION"
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
def clean_text(text):
return ''.join(c for c in text if c.isprintable() or c in "\\n\\r\\t")
def generate_source_txt():
valid_files = []
main_dir = os.path.join(PROJECT_PATH, "src", "main")
if not os.path.exists(main_dir):
print("Error: src/main directory not found!")
return
for root, dirs, files in os.walk(main_dir):
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
for file in files:
if os.path.splitext(file)[1] in INCLUDE_EXT:
file_path = os.path.join(root, file)
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
code_lines = [l for l in lines if l.strip() and not l.strip().startswith("//")]
if len(code_lines) >= MIN_LINE_COUNT:
valid_files.append(file_path)
except:
continue
valid_files.sort(key=lambda x: os.path.getsize(x), reverse=True)
with open(OUTPUT_TXT, "w", encoding="utf-8-sig") as f:
f.write(f"\{SOFTWARE_NAME} \{SOFTWARE_VERSION} 核心源码 - 著作权人:\{COPYRIGHT_OWNER}\\n\\n")
for idx, file_path in enumerate(valid_files, 1):
f.write(f"\\n{'='*60}\\n")
f.write(f"文件 \{idx}\{file_path.replace(PROJECT_PATH, '')}\\n")
f.write(f"{'='*60}\\n\\n")
try:
try:
with open(file_path, "r", encoding="utf-8") as src_f:
content = clean_text(src_f.read())
except UnicodeDecodeError:
with open(file_path, "r", encoding="gbk") as src_f:
content = clean_text(src_f.read())
f.write(content)
f.write("\\n\\n")
except Exception as e:
f.write(f"文件读取失败:\{str(e)}\\n\\n")
continue
print(f"有效源码文件数:\{len(valid_files)}")
print(f"纯文本文件路径:\{os.path.abspath(OUTPUT_TXT)}")
if __name__ == "__main__":
generate_source_txt()
GEN_EOF
python3 generate_source.py
if [ ! -f "PowerBell_Core_Source.txt" ]; then
red_echo "纯文本源码生成失败!"
exit 1
fi
# 步骤3生成带版本号页眉的HTML
blue_echo -e "\n==== 3/7 生成带${NEW_VERSION}页眉的HTML ===="
cat > txt2html.py << TXT_EOF
import os
TXT_FILE = "PowerBell_Core_Source.txt"
HTML_FILE = "PowerBell_Source.html"
SOFTWARE_NAME = "$SOFTWARE_NAME"
SOFTWARE_VERSION = "$NEW_VERSION"
COPYRIGHT_OWNER = "$COPYRIGHT_OWNER"
LINES_PER_PAGE = $LINES_PER_PAGE
CSS_STYLE = """
<style>
@page {{
size: A4;
margin: 10mm 5mm;
@top-center {{
content: "{} {} - 源代码(著作权人:{}";
font-family: 'WenQuanYi Micro Hei', monospace;
font-size: 9pt;
font-weight: bold;
}}
@bottom-center {{
content: "页码 " counter(page) " / " counter(pages);
font-family: 'WenQuanYi Micro Hei', monospace;
font-size: 9pt;
}}
}}
body {{
font-family: 'WenQuanYi Micro Hei', monospace;
font-size: 9pt;
line-height: 1.1;
margin: 0;
padding: 5mm 0 0 0;
counter-reset: code-line;
}}
.file-header {{
background: #f0f0f0;
padding: 3px;
margin: 6px 0;
font-weight: bold;
font-size: 10pt;
}}
.code-block {{
white-space: pre;
margin-left: 8px;
line-height: 1.1;
counter-increment: code-line;
}}
.code-block:before {{
content: counter(code-line) " ";
color: #888;
display: inline-block;
width: 30px;
text-align: right;
margin-right: 5px;
}}
.page-break {{ page-break-after: always; counter-reset: code-line; }}
</style>
""".format(SOFTWARE_NAME, SOFTWARE_VERSION, COPYRIGHT_OWNER)
def txt_to_html():
with open(TXT_FILE, "r", encoding="utf-8") as f:
content = f.read()
html_content = "<!DOCTYPE html><html><head><meta charset='utf-8'>" + CSS_STYLE + "</head><body>"
content_lines = content.split("\\n")[2:]
content_clean = "\\n".join(content_lines)
blocks = content_clean.split("====")
line_count = 0
for block in blocks:
if not block.strip():
continue
if "文件 " in block and ":" in block:
file_header = block.split("\\n")[0].strip() if "\\n" in block else block.strip()
html_content += f"<div class='file-header'>\{file_header}</div>"
code_part = block.split("\\n")[1:] if "\\n" in block else []
block = "\\n".join(code_part)
code_lines = block.split("\\n")
for line in code_lines:
if line.strip() or line_count > 0:
line_count += 1
html_content += f"<div class='code-block'>\{line}</div>"
if line_count >= LINES_PER_PAGE:
html_content += "<div class='page-break'></div>"
line_count = 0
html_content += "</body></html>"
with open(HTML_FILE, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"HTML文件路径\{os.path.abspath(HTML_FILE)}")
if __name__ == "__main__":
txt_to_html()
TXT_EOF
python3 txt2html.py
if [ ! -f "PowerBell_Source.html" ]; then
red_echo "HTML文件生成失败"
exit 1
fi
# 步骤4生成完整PDF
blue_echo -e "\n==== 4/7 生成完整PDF版本${NEW_VERSION} ===="
wkhtmltopdf --page-size A4 \
--margin-top 15mm --margin-bottom 15mm --margin-left 5mm --margin-right 5mm \
--encoding utf-8 \
--no-images --disable-javascript \
--enable-local-file-access \
--no-stop-slow-scripts \
PowerBell_Source.html PowerBell_soft_full.pdf
if [ ! -f "PowerBell_soft_full.pdf" ]; then
red_echo "完整PDF生成失败"
exit 1
fi
# 步骤5截取60页
blue_echo -e "\n==== 5/7 截取前30+后30页 ===="
TOTAL_PAGES=\$(pdfinfo PowerBell_soft_full.pdf | grep "Pages" | awk '{print \$2}')
green_echo "源码完整PDF总页数\$TOTAL_PAGES 页"
if [ "\$TOTAL_PAGES" -le 60 ]; then
cp PowerBell_soft_full.pdf PowerBell_软著源码_${NEW_VERSION}_60页.pdf
green_echo "源码不足60页直接使用完整PDF"
else
pdftk PowerBell_soft_full.pdf cat 1-30 output PowerBell_前30页.pdf
START_PAGE=\$((TOTAL_PAGES - 29))
pdftk PowerBell_soft_full.pdf cat \$START_PAGE-\$TOTAL_PAGES output PowerBell_后30页.pdf
pdftk PowerBell_前30页.pdf PowerBell_后30页.pdf cat output PowerBell_软著源码_${NEW_VERSION}_60页.pdf
rm -f PowerBell_前30页.pdf PowerBell_后30页.pdf
green_echo "源码超过60页已截取前30页+后30页合并为60页"
fi
# 步骤6验证规范
blue_echo -e "\n==== 6/7 验证${NEW_VERSION}版本PDF规范 ===="
FINAL_PAGES=\$(pdfinfo PowerBell_软著源码_${NEW_VERSION}_60页.pdf | grep "Pages" | awk '{print \$2}')
green_echo "最终PDF页数\$FINAL_PAGES 页"
green_echo "每页代码行数:\$LINES_PER_PAGE 行≥50行"
green_echo "页眉信息:$SOFTWARE_NAME $NEW_VERSION - 源代码(著作权人:$COPYRIGHT_OWNER"
# 步骤7清理临时文件
blue_echo -e "\n==== 7/7 清理临时文件 ===="
rm -f generate_source.py txt2html.py PowerBell_Core_Source.txt PowerBell_Source.html PowerBell_soft_full.pdf
green_echo "临时文件清理完成!"
# 输出结果
green_echo -e "\n====================================="
green_echo "$SOFTWARE_NAME $NEW_VERSION 软著PDF生成成功🎉"
green_echo "📄 最终文件:\$(pwd)/PowerBell_软著源码_${NEW_VERSION}_60页.pdf"
green_echo "💡 可直接提交软著登记,无需手动修改!"
green_echo "====================================="
EOF
# 4. 赋予执行权限并运行
chmod +x build_copyright_pdf_temp.sh
blue_echo -e "\n==== 开始生成${NEW_VERSION}版本PDF ===="
./build_copyright_pdf_temp.sh
# 5. 删除临时主脚本(可选,保留则注释此行)
rm -f build_copyright_pdf_temp.sh
green_echo -e "\n==== 操作完成!${NEW_VERSION}版本PDF已生成 ===="

View File

@@ -67,6 +67,12 @@
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# ============================== 必要补充规则 ==============================
# OkHttp 4.4.1 补充规则Java 7 兼容)
-keep class okhttp3.internal.concurrent.** { *; }
-keep class okhttp3.internal.connection.** { *; }
-dontwarn okhttp3.internal.concurrent.TaskRunner
-dontwarn okhttp3.internal.connection.RealCall
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
tools:replace="android:icon"
android:icon="@drawable/ic_launcher_beta">
</application>
</manifest>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name_cn1">能源钟★</string>
<string name="app_name_cn2">泡额呗额☆</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">PowerBell+</string>
</resources>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<shortcut
android:shortcutId="switchto_en1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_en1"
android:shortcutLongLabel="@string/switchto_en1"
android:shortcutDisabledMessage="@string/en1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_en1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<!--<shortcut
android:shortcutId="switchto_cn1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn1"
android:shortcutLongLabel="@string/switchto_cn1"
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>-->
<shortcut
android:shortcutId="switchto_cn2"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn2"
android:shortcutLongLabel="@string/switchto_cn2"
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn2" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<shortcut
android:shortcutId="switchto_en1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_en1"
android:shortcutLongLabel="@string/switchto_en1"
android:shortcutDisabledMessage="@string/en1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_en1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="switchto_cn1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn1"
android:shortcutLongLabel="@string/switchto_cn1"
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<!--<shortcut
android:shortcutId="switchto_cn2"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn2"
android:shortcutLongLabel="@string/switchto_cn2"
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn2" />
<categories android:name="android.shortcut.conversation" />
</shortcut>-->
</shortcuts>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 切换启动入口的快捷菜单 -->
<!--<shortcut
android:shortcutId="switchto_en1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_en1"
android:shortcutLongLabel="@string/switchto_en1"
android:shortcutDisabledMessage="@string/en1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_en1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>-->
<shortcut
android:shortcutId="switchto_cn1"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn1"
android:shortcutLongLabel="@string/switchto_cn1"
android:shortcutDisabledMessage="@string/cn1_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn1" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="switchto_cn2"
android:enabled="true"
android:icon="@drawable/ic_launcher"
android:shortcutShortLabel="@string/switchto_cn2"
android:shortcutLongLabel="@string/switchto_cn2"
android:shortcutDisabledMessage="@string/cn2_switch_disabled">
<intent
android:action="cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"
android:targetPackage="cc.winboll.studio.powerbell.beta"
android:targetClass="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"
android:data="switchto_cn2" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>

View File

@@ -0,0 +1,332 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
<!-- 此应用可显示在其他应用上方 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- 运行“specialUse”类型的前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<!-- 开机启动 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- 显示通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- PACKAGE_USAGE_STATS -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<!-- BATTERY_STATS -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<!-- 计算应用存储空间 -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<!-- 请求忽略电池优化 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false"/>
<queries>
<package android:name="com.miui.securitycenter"/>
</queries>
<application
android:name=".App"
android:process=":main"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme_Default"
android:persistent="true"
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:process=":main"
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:launchMode="singleTask"/>
<activity-alias
android:name=".MainActivityEN1"
android:targetActivity=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmainen1"/>
</activity-alias>
<activity-alias
android:name=".MainActivityCN1"
android:targetActivity=".MainActivity"
android:exported="true"
android:label="@string/app_name_cn1"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn1"/>
</activity-alias>
<activity-alias
android:name=".MainActivityCN2"
android:targetActivity=".MainActivity"
android:exported="true"
android:label="@string/app_name_cn2"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn2"/>
</activity-alias>
<activity
android:process=":main"
android:name=".activities.CrashActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.BackgroundSettingsActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/>
<data android:mimeType="image/webp"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
<activity
android:process=":main"
android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.PixelPickerActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name=".activities.SettingsActivity"
android:exported="false"/>
<activity
android:process=":main"
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
android:exported="false"/>
<activity
android:process=":main"
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"/>
<receiver
android:process=":main"
android:name=".receivers.MainReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.POWER_CONNECTED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter>
</receiver>
<service
android:name=".services.ControlCenterService"
android:priority="1000"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="后台核心功能运行、持续保活"/>
</service>
<service
android:name=".services.AssistantService"
android:enabled="true"
android:exported="false"
android:process=":assistant"
android:stopWithTask="false"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行"/>
</service>
<service
android:name=".services.TTSPlayService"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"/>
<service
android:name=".services.ThoughtfulService"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
android:process=":main">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.powerbell.activities.AboutActivity"/>
</application>
</manifest>

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