Compare commits

..

No commits in common. 'master' and 'xuye_branch' have entirely different histories.

24
src/.gitignore vendored

@ -1,15 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# generated files
bin/
gen/
# Local configuration file (sdk path, etc)
project.properties
.settings/
.classpath
.project

@ -2,14 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 前台服务权限 (Android 9+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 允许查询窗口状态 (可选,为了更好适配) -->
<uses-permission android:name="android.permission.GET_TASKS"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
@ -18,16 +10,12 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- 原有的权限可能已经存在,请确保补充以下权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 针对 Android 13+ (API 33+) 的图片读取权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- 通知权限:便签时间提醒需在设定时间弹出通知 (Android 13+ 需在应用信息中开启) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 声明相机硬件特性(非强制,但推荐) -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
@ -38,24 +26,17 @@
android:theme="@style/NoteTheme"
tools:targetApi="31">
<!-- 开场动画:作为启动入口 -->
<!-- 新的入口 -->
<activity
android:name=".ui.SplashActivity"
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/SplashTheme"
android:screenOrientation="portrait">
android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <!-- 注意 Theme -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 主界面:由 SplashActivity 跳转 -->
<activity
android:name=".ui.MainActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<!-- 旧的 Activity保留声明去掉 intent-filter -->
<activity
android:name=".ui.NotesListActivity"
@ -107,11 +88,7 @@
android:resource="@xml/searchable" />
</activity>
<service
android:name=".tool.FloatingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
@ -158,10 +135,8 @@
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" />
<receiver
android:name=".ui.NoteReminderReceiver"
android:exported="false" />
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
@ -170,19 +145,6 @@
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
</activity>
<activity
android:name=".ui.TodoEditActivity"
android:exported="false"
android:theme="@style/TodoEditTheme" />
<receiver
android:name=".todo.TodoAlarmReceiver"
android:exported="false" />
<receiver
android:name=".todo.TodoGeofenceReceiver"
android:exported="false" />
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"

@ -0,0 +1,190 @@
Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

@ -0,0 +1,23 @@
[中文]
1. MiCode便签是小米便签的社区开源版由MIUI团队(www.miui.com) 发起并贡献第一批代码遵循NOTICE文件所描述的开源协议
今后为MiCode社区(www.micode.net) 拥有,并由社区发布和维护。
2. Bug反馈和跟踪请访问Github,
https://github.com/MiCode/Notes/issues?sort=created&direction=desc&state=open
3. 功能建议和综合讨论请访问MiCode,
http://micode.net/forum.php?mod=forumdisplay&fid=38
[English]
1. MiCode Notes is open source edition of XM notepad, it's first initiated and sponsored by MIUI team (www.miui.com).
It's opened under license described by NOTICE file. It's owned by the MiCode community (www.micode.net). In future,
the MiCode community will release and maintain this project.
2. Regarding issue tracking, please visit Github,
https://github.com/MiCode/Notes/issues?sort=created&direction=desc&state=open
3. Regarding feature request and general discussion, please visit Micode forum,
http://micode.net/forum.php?mod=forumdisplay&fid=38

@ -1 +0,0 @@
/build

@ -1,79 +0,0 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "net.micode.notes"
// 修改这里:匹配你下载的 SDK 30
compileSdk = 34
defaultConfig {
applicationId = "net.micode.notes"
minSdk = 30
// 修改这里:匹配你下载的 SDK 30
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// === 这里是新增的代码,用来解决 Jar 包冲突报错 ===
packaging {
resources {
excludes += "META-INF/DEPENDENCIES"
excludes += "META-INF/NOTICE"
excludes += "META-INF/LICENSE"
excludes += "META-INF/LICENSE.txt"
excludes += "META-INF/NOTICE.txt"
}
}
// ==============================================
}
dependencies {
// === 修改部分:使用兼容 SDK 30 的旧版本库 ===
// 替换掉了原本的 libs.appcompat 等引用,改为写死的旧版本号
// ViewPager2 用于左右滑动
implementation("androidx.recyclerview:recyclerview:1.3.0")
implementation("androidx.viewpager2:viewpager2:1.0.0")
// Material Design 用于底部导航栏
implementation("com.google.android.material:material:1.9.0")
// Fragment 核心库 (确保使用 AndroidX 版本)
implementation("androidx.fragment:fragment:1.5.7")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("com.google.android.material:material:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
// 如果你需要 Activity 的新特性,也需要指定旧版本,否则默认会拉取最新的导致报错
implementation("androidx.activity:activity:1.2.4")
// Google Play Services - 用于地理围栏
implementation("com.google.android.gms:play-services-location:21.0.1")
// === 保持不变:你的 Jar 包路径 ===
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
// === 测试库也建议指定旧版本,防止冲突 ===
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
}

Binary file not shown.

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -1,26 +0,0 @@
package net.micode.notes;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("net.micode.notes", appContext.getPackageName());
}
}

@ -1,153 +0,0 @@
package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.todo.TodoItem;
import java.util.ArrayList;
import java.util.List;
/**
* 访 (CRUD)
*/
public class TodoDao {
private static final String TAG = "TodoDao";
private static final String COL_ID = "id";
private static final String COL_CONTENT = "content";
private static final String COL_IS_DONE = "is_done";
private static final String COL_REMINDER_TYPE = "reminder_type";
private static final String COL_REMINDER_TIMESTAMP = "reminder_timestamp";
private static final String COL_LATITUDE = "latitude";
private static final String COL_LONGITUDE = "longitude";
private static final String COL_LOCATION_NAME = "location_name";
private static final String COL_CREATED_TIME = "created_time";
private final NotesDatabaseHelper mHelper;
public TodoDao(Context context) {
mHelper = NotesDatabaseHelper.getInstance(context);
}
public long insert(TodoItem item) {
SQLiteDatabase db = mHelper.getWritableDatabase();
ContentValues cv = toContentValues(item);
long id = db.insert(TABLE.TODO_ITEMS, null, cv);
Log.d(TAG, "insert todo id=" + id);
return id;
}
public int update(TodoItem item) {
if (item.getId() <= 0) return 0;
SQLiteDatabase db = mHelper.getWritableDatabase();
ContentValues cv = toContentValues(item);
return db.update(TABLE.TODO_ITEMS, cv, COL_ID + "=?", new String[]{String.valueOf(item.getId())});
}
public int delete(long id) {
SQLiteDatabase db = mHelper.getWritableDatabase();
return db.delete(TABLE.TODO_ITEMS, COL_ID + "=?", new String[]{String.valueOf(id)});
}
public int deleteByIds(List<Long> ids) {
if (ids == null || ids.isEmpty()) return 0;
SQLiteDatabase db = mHelper.getWritableDatabase();
StringBuilder sb = new StringBuilder(COL_ID).append(" IN (");
for (int i = 0; i < ids.size(); i++) {
sb.append(i > 0 ? "," : "").append(ids.get(i));
}
sb.append(")");
return db.delete(TABLE.TODO_ITEMS, sb.toString(), null);
}
public TodoItem getById(long id) {
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, COL_ID + "=?", new String[]{String.valueOf(id)}, null, null, null)) {
if (c != null && c.moveToFirst()) {
return fromCursor(c);
}
}
return null;
}
public List<TodoItem> getAll() {
List<TodoItem> list = new ArrayList<>();
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, null, null, null, null, COL_IS_DONE + " ASC, " + COL_CREATED_TIME + " DESC")) {
if (c != null) {
while (c.moveToNext()) {
list.add(fromCursor(c));
}
}
}
return list;
}
public List<TodoItem> getUndone() {
List<TodoItem> list = new ArrayList<>();
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, COL_IS_DONE + "=0", null, null, null, COL_CREATED_TIME + " DESC")) {
if (c != null) {
while (c.moveToNext()) {
list.add(fromCursor(c));
}
}
}
return list;
}
public List<TodoItem> getDone() {
List<TodoItem> list = new ArrayList<>();
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, COL_IS_DONE + "=1", null, null, null, COL_CREATED_TIME + " DESC")) {
if (c != null) {
while (c.moveToNext()) {
list.add(fromCursor(c));
}
}
}
return list;
}
public int getUndoneCount() {
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.rawQuery("SELECT COUNT(*) FROM " + TABLE.TODO_ITEMS + " WHERE " + COL_IS_DONE + "=0", null)) {
if (c != null && c.moveToFirst()) {
return c.getInt(0);
}
}
return 0;
}
private ContentValues toContentValues(TodoItem item) {
ContentValues cv = new ContentValues();
cv.put(COL_CONTENT, item.getContent());
cv.put(COL_IS_DONE, item.isDone() ? 1 : 0);
cv.put(COL_REMINDER_TYPE, item.getReminderType());
cv.put(COL_REMINDER_TIMESTAMP, item.getReminderTimestamp());
cv.put(COL_LATITUDE, item.getLatitude());
cv.put(COL_LONGITUDE, item.getLongitude());
cv.put(COL_LOCATION_NAME, item.getLocationName());
cv.put(COL_CREATED_TIME, item.getCreatedTime());
return cv;
}
private TodoItem fromCursor(Cursor c) {
TodoItem item = new TodoItem();
item.setId(c.getLong(c.getColumnIndexOrThrow(COL_ID)));
item.setContent(c.getString(c.getColumnIndexOrThrow(COL_CONTENT)));
item.setDone(c.getInt(c.getColumnIndexOrThrow(COL_IS_DONE)) == 1);
item.setReminderType(c.getInt(c.getColumnIndexOrThrow(COL_REMINDER_TYPE)));
item.setReminderTimestamp(c.getLong(c.getColumnIndexOrThrow(COL_REMINDER_TIMESTAMP)));
item.setLatitude(c.getDouble(c.getColumnIndexOrThrow(COL_LATITUDE)));
item.setLongitude(c.getDouble(c.getColumnIndexOrThrow(COL_LONGITUDE)));
item.setLocationName(c.getString(c.getColumnIndexOrThrow(COL_LOCATION_NAME)));
item.setCreatedTime(c.getLong(c.getColumnIndexOrThrow(COL_CREATED_TIME)));
return item;
}
}

@ -1,130 +0,0 @@
package net.micode.notes.todo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import java.util.ArrayList;
import java.util.Random;
/**
* View Canvas
*/
public class ConfettiView extends View {
/** 粒子数量,铺满整屏 */
private static final int PARTICLE_COUNT = 260;
private static final long DURATION_MS = 2800;
private final ArrayList<Particle> mParticles = new ArrayList<>();
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Random mRandom = new Random();
private long mStartTime;
private static class Particle {
float x, y;
float vx, vy;
float size;
int color;
float rotation;
float rotationSpeed;
}
public ConfettiView(Context context) {
super(context);
init();
}
public ConfettiView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint.setStyle(Paint.Style.FILL);
}
public void start() {
mParticles.clear();
setVisibility(VISIBLE);
// 先设为 VISIBLE 再 post确保布局后 getWidth/getHeight 有值GONE 时宽高为 0 会直接 return
post(this::initParticlesIfSized);
}
private void initParticlesIfSized() {
initParticlesIfSized(0);
}
private static final int MAX_LAYOUT_RETRY = 3;
private void initParticlesIfSized(int retryCount) {
int w = getWidth();
int h = getHeight();
if (w <= 0 || h <= 0) {
if (retryCount < MAX_LAYOUT_RETRY) {
requestLayout();
post(() -> initParticlesIfSized(retryCount + 1));
}
return;
}
int[] colors = {
Color.parseColor("#FF6B6B"),
Color.parseColor("#4ECDC4"),
Color.parseColor("#FFE66D"),
Color.parseColor("#95E1D3"),
Color.parseColor("#F38181"),
Color.parseColor("#AA96DA"),
Color.parseColor("#FCBAD3"),
Color.parseColor("#A8D8EA"),
};
for (int i = 0; i < PARTICLE_COUNT; i++) {
Particle p = new Particle();
p.x = (mRandom.nextFloat() - 0.1f) * (w * 1.2f);
p.y = -mRandom.nextFloat() * h * 0.6f;
p.vx = (mRandom.nextFloat() - 0.5f) * 10;
p.vy = 5 + mRandom.nextFloat() * 14;
p.size = 8 + mRandom.nextFloat() * 12;
p.color = colors[mRandom.nextInt(colors.length)];
p.rotation = mRandom.nextFloat() * 360;
p.rotationSpeed = (mRandom.nextFloat() - 0.5f) * 24;
mParticles.add(p);
}
mStartTime = System.currentTimeMillis();
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
if (mParticles.isEmpty()) return;
long elapsed = System.currentTimeMillis() - mStartTime;
if (elapsed >= DURATION_MS) {
setVisibility(GONE);
return;
}
float progress = (float) elapsed / DURATION_MS;
AccelerateInterpolator interpolator = new AccelerateInterpolator();
float alpha = 1f - interpolator.getInterpolation(progress);
for (Particle p : mParticles) {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.3f;
p.rotation += p.rotationSpeed;
mPaint.setColor(p.color);
mPaint.setAlpha((int) (255 * alpha));
canvas.save();
canvas.translate(p.x, p.y);
canvas.rotate(p.rotation);
canvas.drawRect(-p.size / 2, -p.size / 4, p.size / 2, p.size / 4, mPaint);
canvas.restore();
}
invalidate();
}
}

@ -1,251 +0,0 @@
package net.micode.notes.todo;
import android.graphics.Paint;
import android.view.LayoutInflater;
import androidx.core.content.ContextCompat;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
* <p>
* RecyclerView Adapter
* 线 OnItemClickListener OnItemLongClickListener
* </p>
*
* @see TodoFragment
* @see TodoItem
*/
public class TodoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_SECTION = 0;
private static final int VIEW_TYPE_ITEM = 1;
private final List<Object> mItems = new ArrayList<>();
private final Set<Integer> mSelectedPositions = new HashSet<>();
private boolean mChoiceMode;
private OnItemClickListener mItemClickListener;
private OnItemLongClickListener mItemLongClickListener;
public interface OnItemClickListener {
void onItemClick(TodoItem item, int adapterPosition);
void onCheckChanged(TodoItem item, boolean checked);
}
public interface OnItemLongClickListener {
void onItemLongClick(TodoItem item, int adapterPosition);
}
/**
*
*
* @param undone
* @param done
*/
public void setData(List<TodoItem> undone, List<TodoItem> done) {
mItems.clear();
if (!undone.isEmpty()) {
mItems.add("undone");
mItems.addAll(undone);
}
if (!done.isEmpty()) {
mItems.add("done");
mItems.addAll(done);
}
notifyDataSetChanged();
}
/** 设置 item 点击与勾选变更监听器 */
public void setOnItemClickListener(OnItemClickListener l) { mItemClickListener = l; }
/** 设置 item 长按监听器 */
public void setOnItemLongClickListener(OnItemLongClickListener l) { mItemLongClickListener = l; }
/**
*
*
* @param mode true
*/
public void setChoiceMode(boolean mode) {
mChoiceMode = mode;
mSelectedPositions.clear();
notifyDataSetChanged();
}
/** 判断是否处于多选模式 */
public boolean isChoiceMode() { return mChoiceMode; }
/**
*
*
* @param position
* @param checked true
*/
public void setChecked(int position, boolean checked) {
if (checked) mSelectedPositions.add(position);
else mSelectedPositions.remove(position);
notifyItemChanged(position);
}
/** 判断指定位置是否被选中 */
public boolean isChecked(int position) { return mSelectedPositions.contains(position); }
/** 获取当前选中的待办数量 */
public int getSelectedCount() { return mSelectedPositions.size(); }
/**
* ID
*/
public List<Long> getSelectedIds() {
List<Long> ids = new ArrayList<>();
for (Integer pos : mSelectedPositions) {
Object o = mItems.get(pos);
if (o instanceof TodoItem) ids.add(((TodoItem) o).getId());
}
return ids;
}
/**
* TodoItem
*
* @param select true false
*/
public void selectAll(boolean select) {
mSelectedPositions.clear();
if (select) {
for (int i = 0; i < mItems.size(); i++) {
if (mItems.get(i) instanceof TodoItem) mSelectedPositions.add(i);
}
}
notifyDataSetChanged();
}
/**
*
*/
public boolean isAllSelected() {
int itemCount = 0;
for (Object o : mItems) if (o instanceof TodoItem) itemCount++;
return itemCount > 0 && mSelectedPositions.size() == itemCount;
}
private boolean isSection(int position) {
return mItems.get(position) instanceof String;
}
private TodoItem getItemAt(int position) {
Object o = mItems.get(position);
return o instanceof TodoItem ? (TodoItem) o : null;
}
@Override
public int getItemViewType(int position) {
return isSection(position) ? VIEW_TYPE_SECTION : VIEW_TYPE_ITEM;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_SECTION) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_todo_section, parent, false);
return new SectionHolder(v);
} else {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_todo, parent, false);
return new ItemHolder(v);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof SectionHolder) {
String section = (String) mItems.get(position);
((SectionHolder) holder).bind(section.equals("undone")
? holder.itemView.getContext().getString(R.string.todo_section_undone)
: holder.itemView.getContext().getString(R.string.todo_section_done));
} else if (holder instanceof ItemHolder) {
TodoItem item = getItemAt(position);
if (item != null) {
((ItemHolder) holder).bind(item, mChoiceMode, mSelectedPositions.contains(position));
}
}
}
@Override
public int getItemCount() { return mItems.size(); }
private class SectionHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
SectionHolder(View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tv_section_title);
}
void bind(String title) {
tvTitle.setText(title);
}
}
private class ItemHolder extends RecyclerView.ViewHolder {
CheckBox cbTodo;
TextView tvContent;
ItemHolder(View itemView) {
super(itemView);
cbTodo = itemView.findViewById(R.id.cb_todo);
tvContent = itemView.findViewById(R.id.tv_todo_content);
}
void bind(TodoItem item, boolean choiceMode, boolean checked) {
// 先移除监听器,避免 setChecked 触发复用前的旧回调导致错更新或闪退
cbTodo.setOnCheckedChangeListener(null);
cbTodo.setChecked(item.isDone());
tvContent.setText(item.getContent());
if (item.isDone()) {
tvContent.setPaintFlags(tvContent.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
tvContent.setTextColor(ContextCompat.getColor(itemView.getContext(), android.R.color.darker_gray));
} else {
tvContent.setPaintFlags(tvContent.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
tvContent.setTextColor(ContextCompat.getColor(itemView.getContext(), R.color.text_primary));
}
cbTodo.setVisibility(View.VISIBLE);
if (choiceMode) {
cbTodo.setChecked(checked);
cbTodo.setOnCheckedChangeListener((b, isChecked) -> setChecked(getAdapterPosition(), isChecked));
} else {
cbTodo.setOnCheckedChangeListener((b, isChecked) -> {
if (mItemClickListener != null) mItemClickListener.onCheckChanged(item, isChecked);
});
}
itemView.setOnClickListener(v -> {
if (choiceMode) {
setChecked(getAdapterPosition(), !mSelectedPositions.contains(getAdapterPosition()));
} else if (mItemClickListener != null) {
mItemClickListener.onItemClick(item, getAdapterPosition());
}
});
itemView.setOnLongClickListener(v -> {
if (mItemLongClickListener != null) {
mItemLongClickListener.onItemLongClick(item, getAdapterPosition());
}
return false;
});
}
}
}

@ -1,61 +0,0 @@
package net.micode.notes.todo;
/**
* 广
* <p>
* AlarmManager 广
* Android 8.0+
* </p>
*
* @see TodoReminderManager
*/
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import net.micode.notes.R;
import net.micode.notes.ui.MainActivity;
public class TodoAlarmReceiver extends BroadcastReceiver {
private static final String CHANNEL_ID = "todo_reminder";
@Override
public void onReceive(Context context, Intent intent) {
long todoId = intent.getLongExtra("todo_id", 0);
String content = intent.getStringExtra("todo_content");
if (content == null) content = "待办提醒";
createNotificationChannel(context);
Intent openIntent = new Intent(context, MainActivity.class);
openIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pi = PendingIntent.getActivity(context, (int) todoId, openIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.icon_app)
.setContentTitle("待办提醒")
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pi)
.setAutoCancel(true);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.notify((int) todoId, builder.build());
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
"待办提醒", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.createNotificationChannel(channel);
}
}
}

@ -1,97 +0,0 @@
package net.micode.notes.todo;
/**
* 广
* <p>
* Geofencing 广
* 使 Google Play Services Geofencing API
* </p>
*
* @see TodoReminderManager
* @see com.google.android.gms.location.GeofencingClient
*/
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingEvent;
import net.micode.notes.R;
import net.micode.notes.ui.MainActivity;
import java.util.List;
public class TodoGeofenceReceiver extends BroadcastReceiver {
private static final String TAG = "TodoGeofenceReceiver";
private static final String CHANNEL_ID = "todo_geofence";
@Override
public void onReceive(Context context, Intent intent) {
GeofencingEvent event = GeofencingEvent.fromIntent(intent);
if (event == null) return;
if (event.hasError()) {
Log.e(TAG, "Geofence error: " + event.getErrorCode());
return;
}
int transition = event.getGeofenceTransition();
if (transition != Geofence.GEOFENCE_TRANSITION_ENTER) return;
List<Geofence> triggering = event.getTriggeringGeofences();
if (triggering == null || triggering.isEmpty()) return;
for (Geofence g : triggering) {
String requestId = g.getRequestId();
if (requestId.startsWith("todo_")) {
long todoId = Long.parseLong(requestId.substring(5));
String content = intent.getStringExtra("todo_content");
String locationName = intent.getStringExtra("location_name");
if (content == null) {
net.micode.notes.data.TodoDao dao = new net.micode.notes.data.TodoDao(context);
net.micode.notes.todo.TodoItem item = dao.getById(todoId);
content = item != null ? item.getContent() : "待办";
if (item != null && locationName == null) locationName = item.getLocationName();
}
String title = "到达" + (locationName != null ? locationName : "目标地点");
showNotification(context, (int) todoId, title, content);
}
}
}
private void showNotification(Context context, int id, String title, String content) {
createNotificationChannel(context);
Intent openIntent = new Intent(context, MainActivity.class);
openIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pi = PendingIntent.getActivity(context, id, openIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.icon_app)
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pi)
.setAutoCancel(true);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.notify(id, builder.build());
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
"待办地点提醒", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.createNotificationChannel(channel);
}
}
}

@ -1,62 +0,0 @@
package net.micode.notes.todo;
/**
*
*/
public class TodoItem {
/** 提醒类型:无 */
public static final int REMINDER_NONE = 0;
/** 提醒类型:时间 */
public static final int REMINDER_TIME = 1;
/** 提醒类型:地点 */
public static final int REMINDER_LOCATION = 2;
private long id;
private String content;
private boolean isDone;
private int reminderType;
private long reminderTimestamp;
private double latitude;
private double longitude;
private String locationName;
private long createdTime;
public TodoItem() {
this.id = 0;
this.content = "";
this.isDone = false;
this.reminderType = REMINDER_NONE;
this.reminderTimestamp = 0;
this.latitude = 0;
this.longitude = 0;
this.locationName = "";
this.createdTime = System.currentTimeMillis();
}
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content != null ? content : ""; }
public boolean isDone() { return isDone; }
public void setDone(boolean done) { isDone = done; }
public int getReminderType() { return reminderType; }
public void setReminderType(int reminderType) { this.reminderType = reminderType; }
public long getReminderTimestamp() { return reminderTimestamp; }
public void setReminderTimestamp(long reminderTimestamp) { this.reminderTimestamp = reminderTimestamp; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public String getLocationName() { return locationName; }
public void setLocationName(String locationName) { this.locationName = locationName != null ? locationName : ""; }
public long getCreatedTime() { return createdTime; }
public void setCreatedTime(long createdTime) { this.createdTime = createdTime; }
}

@ -1,121 +0,0 @@
package net.micode.notes.todo;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingClient;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.location.LocationServices;
import net.micode.notes.data.TodoDao;
import java.util.ArrayList;
import java.util.List;
/**
* (AlarmManager) + (Geofencing)
*/
public class TodoReminderManager {
private static final String TAG = "TodoReminderManager";
private static final float GEOFENCE_RADIUS_METERS = 100f;
public static void scheduleReminders(Context context, TodoItem item) {
if (item == null || item.getId() <= 0) return;
cancelReminders(context, item.getId());
if (item.getReminderType() == TodoItem.REMINDER_TIME && item.getReminderTimestamp() > 0) {
scheduleTimeReminder(context, item);
} else if (item.getReminderType() == TodoItem.REMINDER_LOCATION
&& item.getLatitude() != 0 && item.getLongitude() != 0) {
scheduleGeofenceReminder(context, item);
}
}
public static void cancelReminders(Context context, long todoId) {
cancelTimeReminder(context, todoId);
cancelGeofenceReminder(context, todoId);
}
private static void scheduleTimeReminder(Context context, TodoItem item) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (am == null) return;
if (item.getReminderTimestamp() <= System.currentTimeMillis()) return;
Intent intent = new Intent(context, TodoAlarmReceiver.class);
intent.putExtra("todo_id", item.getId());
intent.putExtra("todo_content", item.getContent());
PendingIntent pi = PendingIntent.getBroadcast(context, (int) item.getId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (am.canScheduleExactAlarms()) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
} else {
am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
} else {
am.set(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
}
Log.d(TAG, "Scheduled time reminder for todo " + item.getId());
}
private static void cancelTimeReminder(Context context, long todoId) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (am == null) return;
Intent intent = new Intent(context, TodoAlarmReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(context, (int) todoId, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
am.cancel(pi);
}
private static void scheduleGeofenceReminder(Context context, TodoItem item) {
GeofencingClient client = LocationServices.getGeofencingClient(context);
Geofence geofence = new Geofence.Builder()
.setRequestId("todo_" + item.getId())
.setCircularRegion(item.getLatitude(), item.getLongitude(), GEOFENCE_RADIUS_METERS)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build();
List<Geofence> list = new ArrayList<>();
list.add(geofence);
GeofencingRequest request = new GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofences(list)
.build();
Intent intent = new Intent(context, TodoGeofenceReceiver.class);
intent.putExtra("todo_id", item.getId());
intent.putExtra("todo_content", item.getContent());
intent.putExtra("location_name", item.getLocationName());
PendingIntent pi = PendingIntent.getBroadcast(context, (int) item.getId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
client.addGeofences(request, pi)
.addOnSuccessListener(aVoid -> Log.d(TAG, "Geofence added for todo " + item.getId()))
.addOnFailureListener(e -> Log.e(TAG, "Geofence add failed: " + e.getMessage()));
}
private static void cancelGeofenceReminder(Context context, long todoId) {
GeofencingClient client = LocationServices.getGeofencingClient(context);
List<String> ids = new ArrayList<>();
ids.add("todo_" + todoId);
client.removeGeofences(ids)
.addOnSuccessListener(aVoid -> Log.d(TAG, "Geofence removed for todo " + todoId))
.addOnFailureListener(e -> Log.e(TAG, "Geofence remove failed: " + e.getMessage()));
}
/** 在删除待办时调用,取消所有提醒 */
public static void cancelAllForTodo(Context context, long todoId) {
cancelReminders(context, todoId);
}
}

@ -1,91 +0,0 @@
package net.micode.notes.tool;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import net.micode.notes.R;
/**
*
* <p>
*
* MediaProjection 便
* Android 10+ FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
* </p>
*
* @see FloatingViewManager
* @see ScreenCaptureManager
*/
public class FloatingService extends Service {
private FloatingViewManager mManager;
private ScreenCaptureManager mCaptureManager;
@Override
public IBinder onBind(Intent intent) { return null; }
@Override
public void onCreate() {
super.onCreate();
mManager = new FloatingViewManager(this);
startForegroundServiceNotification();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && intent.hasExtra("data")) {
int resultCode = intent.getIntExtra("resultCode", -1);
Intent data = intent.getParcelableExtra("data");
MediaProjectionManager mm = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
MediaProjection mp = mm.getMediaProjection(resultCode, data);
// 关键:确保这里每次都是 new 出来的
mCaptureManager = new ScreenCaptureManager(this);
mCaptureManager.setMediaProjection(mp);
mManager.setCaptureManager(mCaptureManager);
}
mManager.showFloatingBall();
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
mManager.removeFloatingBall();
}
private void startForegroundServiceNotification() {
String channelId = "inspiration_channel";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId, "灵感球服务", NotificationManager.IMPORTANCE_LOW);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
}
// 1. 构建通知,删掉了那行报错的代码
Notification notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("灵感球已开启")
.setContentText("点击悬浮球,捕捉瞬间灵感")
.setSmallIcon(R.drawable.icon_app)
.build();
// 2. 在这里指定前台服务类型(这是最标准的写法)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10 (Q) 以上支持指定类型
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
} else {
// 老版本直接启动
startForeground(1, notification);
}
}
}

@ -1,302 +0,0 @@
package net.micode.notes.tool;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import net.micode.notes.tool.DataUtils;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.util.Log;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
// 关键:必须导入内部类 NoteColumns 和 DataColumns
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.DataColumns;
import java.io.File;
import java.io.FileOutputStream;
/**
* (Floating Ball Manager)
* <p>
*
* 1. UI
* 2. +
* 3. {@link ScreenCaptureManager} {@link MediaUtils}
*/
public class FloatingViewManager {
private Context mContext;
private WindowManager mWindowManager;
private View mFloatingView;
private WindowManager.LayoutParams mParams;
private ScreenCaptureManager mCaptureManager;
private int mScreenWidth, mScreenHeight;
/**
*
*
* @param context WindowManager DisplayMetrics
*/
public FloatingViewManager(Context context) {
mContext = context;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics m = context.getResources().getDisplayMetrics();
mScreenWidth = m.widthPixels;
mScreenHeight = m.heightPixels;
}
/**
* capture
*
* @param scm null
*/
public void setCaptureManager(ScreenCaptureManager scm) {
this.mCaptureManager = scm;
}
/**
*
* 使 TYPE_APPLICATION_OVERLAY TYPE_PHONE
*/
public void showFloatingBall() {
if (mFloatingView != null) return;
mParams = new WindowManager.LayoutParams();
mParams.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE;
mParams.format = PixelFormat.RGBA_8888;
mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
mParams.gravity = Gravity.LEFT | Gravity.TOP;
mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.x = mScreenWidth;
mParams.y = mScreenHeight / 2;
mFloatingView = LayoutInflater.from(mContext).inflate(R.layout.layout_floating_ball, null);
initTouchListener();
mWindowManager.addView(mFloatingView, mParams);
}
/**
*
*/
private void initTouchListener() {
View ball = mFloatingView.findViewById(R.id.iv_floating_ball);
ball.setOnTouchListener(new View.OnTouchListener() {
private int initialX, initialY;
private float initialTouchX, initialTouchY;
private boolean isDrag;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = mParams.x; initialY = mParams.y;
initialTouchX = event.getRawX(); initialTouchY = event.getRawY();
isDrag = false;
return true;
case MotionEvent.ACTION_MOVE:
int dx = (int) (event.getRawX() - initialTouchX);
int dy = (int) (event.getRawY() - initialTouchY);
if (!isDrag && (Math.abs(dx) > 20 || Math.abs(dy) > 20)) isDrag = true;
if (isDrag) {
mParams.x = initialX + dx; mParams.y = initialY + dy;
mWindowManager.updateViewLayout(mFloatingView, mParams);
}
return true;
case MotionEvent.ACTION_UP:
if (!isDrag) onBallClick(); else snapToEdge();
return true;
}
return false;
}
});
}
/**
*
*/
private void snapToEdge() {
mParams.x = (mParams.x + mFloatingView.getWidth() / 2 < mScreenWidth / 2) ? 0 : mScreenWidth;
mWindowManager.updateViewLayout(mFloatingView, mParams);
}
/**
*
* 3
*/
private void onBallClick() {
if (mCaptureManager == null) {
Toast.makeText(mContext, "截屏服务未初始化", Toast.LENGTH_SHORT).show();
return;
}
// 1. 视觉与触觉反馈
mFloatingView.animate().scaleX(0.7f).scaleY(0.7f).setDuration(100).withEndAction(() -> {
mFloatingView.animate().scaleX(1.0f).scaleY(1.0f).setDuration(100).start();
}).start();
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE));
// 2. 隐藏小球开始截图
mFloatingView.setVisibility(View.GONE);
// 增加一个“超时保险”:如果 3 秒还没截到图,强行把球变回来
final Handler timeoutHandler = new Handler(Looper.getMainLooper());
final Runnable timeoutRunnable = () -> {
if (mFloatingView.getVisibility() == View.GONE) {
mFloatingView.setVisibility(View.VISIBLE);
Toast.makeText(mContext, "截图响应超时", Toast.LENGTH_SHORT).show();
}
};
timeoutHandler.postDelayed(timeoutRunnable, 3000);
// 3. 执行截图
new Handler(Looper.getMainLooper()).postDelayed(() -> {
mCaptureManager.capture(bitmap -> {
// 截图有回应了,移除超时保险
timeoutHandler.removeCallbacks(timeoutRunnable);
mFloatingView.setVisibility(View.VISIBLE);
if (bitmap != null) {
handleCapturedBitmap(bitmap);
} else {
Toast.makeText(mContext, "截图失败,请重试", Toast.LENGTH_SHORT).show();
}
});
}, 200); // 稍微延长一点等待时间,确保小球完全消失
}
/**
* Bitmap JPEG
*
* @param bitmap null
*/
private void handleCapturedBitmap(Bitmap bitmap) {
try {
File file = MediaUtils.createImageFile(mContext);
try (FileOutputStream out = new FileOutputStream(file)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, out);
}
// 使用 getCanonicalPath() 规范化路径,符合 G.FIO.01
String path = file.getCanonicalPath();
showInputWindow(path);
} catch (Exception e) { e.printStackTrace(); }
}
/**
* saveInspirationToDb
*
* @param imagePath
*/
private void showInputWindow(String imagePath) {
WindowManager.LayoutParams p = new WindowManager.LayoutParams();
p.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE;
p.format = PixelFormat.RGBA_8888;
p.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND;
p.dimAmount = 0.5f;
p.width = WindowManager.LayoutParams.MATCH_PARENT;
p.height = WindowManager.LayoutParams.WRAP_CONTENT;
p.gravity = Gravity.CENTER;
View inputView = LayoutInflater.from(mContext).inflate(R.layout.layout_floating_input, null);
EditText et = inputView.findViewById(R.id.et_inspiration);
inputView.findViewById(R.id.btn_send_inspiration).setOnClickListener(v -> {
String text = et.getText().toString();
saveInspirationToDb(imagePath, text);
mWindowManager.removeView(inputView);
});
mWindowManager.addView(inputView, p);
et.requestFocus();
// 延时弹出软键盘
new Handler(Looper.getMainLooper()).postDelayed(() -> {
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(et, InputMethodManager.SHOW_IMPLICIT);
}, 200);
}
/**
* + 便
* 线 Note Data snippet
*
* @param path
* @param text
*/
private void saveInspirationToDb(String path, String text) {
new Thread(() -> {
try {
// 1. 插入 Note 主表snippet 先占位,插入 Data 后会被触发器覆盖,再在下面按规则写回)
ContentValues v = new ContentValues();
long now = System.currentTimeMillis();
v.put(net.micode.notes.data.Notes.NoteColumns.PARENT_ID, net.micode.notes.data.Notes.ID_ROOT_FOLDER);
v.put(net.micode.notes.data.Notes.NoteColumns.SNIPPET, "");
v.put(net.micode.notes.data.Notes.NoteColumns.TYPE, 0); // TYPE_NOTE
v.put(net.micode.notes.data.Notes.NoteColumns.MODIFIED_DATE, now);
v.put(net.micode.notes.data.Notes.NoteColumns.CREATED_DATE, now);
v.put(net.micode.notes.data.Notes.NoteColumns.LOCAL_MODIFIED, 1);
v.put(net.micode.notes.data.Notes.NoteColumns.BG_COLOR_ID, 0);
Uri uri = mContext.getContentResolver().insert(net.micode.notes.data.Notes.CONTENT_NOTE_URI, v);
if (uri == null) return;
long noteId = android.content.ContentUris.parseId(uri);
// 2. 插入 Data 附表
ContentValues dv = new ContentValues();
dv.put(Notes.DataColumns.NOTE_ID, noteId);
dv.put(Notes.DataColumns.MIME_TYPE, "vnd.android.cursor.item/text_note");
String fullContent = "<img src=\"" + path + "\"/>\n" + (text == null ? "" : text);
dv.put(Notes.DataColumns.CONTENT, fullContent);
dv.put(Notes.DataColumns.DATA1, 0);
mContext.getContentResolver().insert(Notes.CONTENT_DATA_URI, dv);
// 3. 灵感便签标题修复:优先用用户输入的文字作为 snippet仅无文字时用「图片便签」禁止用文件路径
String snippet = DataUtils.computeNoteSnippet(fullContent);
ContentValues uv = new ContentValues();
uv.put(Notes.NoteColumns.SNIPPET, snippet);
mContext.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), uv, null, null);
new Handler(Looper.getMainLooper()).post(() ->
Toast.makeText(mContext, "灵感已存入灵感箱", Toast.LENGTH_SHORT).show());
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
/**
* showFloatingBall
*/
public void removeFloatingBall() {
if (mFloatingView != null) {
mWindowManager.removeView(mFloatingView);
mFloatingView = null;
}
}
}

@ -1,249 +0,0 @@
package net.micode.notes.tool;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
*
* IOUriBitmap
*
*/
public class MediaUtils {
private static final String TAG = "MediaUtils";
private static final String IMAGE_DIR_NAME = "images";
/**
*
* /Android/data//files/Pictures/images/
*/
public static File createImageFile(Context context) throws IOException {
return createImageFileWithExtension(context, ".jpg");
}
/**
* //
*
* @param context getExternalFilesDir
* @param extension ".jpg"".png"
* @return Pictures/images
*/
private static File createImageFileWithExtension(Context context, String extension) throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "NOTE_" + timeStamp + "_";
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (storageDir != null && !storageDir.exists()) {
storageDir.mkdirs();
}
if (extension == null || !extension.startsWith(".")) {
extension = ".jpg";
}
return File.createTempFile(imageFileName, extension, storageDir);
}
/**
* Uri MIME PNG jpg/webp
*
* @param context ContentResolver.getType
* @param uri Uri
* @return ".jpg"".png"
*/
private static String getExtensionFromUri(Context context, Uri uri) {
if (uri == null) return ".jpg";
String mime = context.getContentResolver().getType(uri);
if (mime != null) {
if (mime.contains("png")) return ".png";
if (mime.contains("webp")) return ".webp";
if (mime.contains("gif")) return ".gif";
}
return ".jpg";
}
/**
* Uri // ContentProvider file Uri
* 便 Uri
* @return null
*/
public static String copyUriToInternalStorage(Context context, Uri sourceUri) {
if (context == null || sourceUri == null) return null;
// 1. file:// 方案:截图或部分文件管理器返回的是文件路径,直接复制文件
if ("file".equalsIgnoreCase(sourceUri.getScheme())) {
String path = sourceUri.getPath();
if (path != null && !path.isEmpty()) {
return copyFileToInternalStorage(context, new File(path));
}
Log.e(TAG, "file Uri has no path: " + sourceUri);
return null;
}
// 2. content:// 方案:相册、微信等返回的 Content Uri
ContentResolver contentResolver = context.getContentResolver();
String extension = getExtensionFromUri(context, sourceUri);
// 2a. 优先尝试 openInputStream多数相册/MediaStore 支持)
InputStream inputStream = null;
try {
inputStream = contentResolver.openInputStream(sourceUri);
} catch (SecurityException e) {
Log.w(TAG, "openInputStream SecurityException, try openFileDescriptor: " + e.getMessage());
} catch (java.io.FileNotFoundException e) {
Log.w(TAG, "openInputStream FileNotFoundException, try openFileDescriptor: " + e.getMessage());
}
if (inputStream != null) {
String path = copyStreamToInternalStorage(context, inputStream, extension);
closeQuietly(inputStream);
return path;
}
// 2b. 部分应用(如微信、某些截图)仅支持 openFileDescriptor用此方式读取
try {
android.os.ParcelFileDescriptor pfd = contentResolver.openFileDescriptor(sourceUri, "r");
if (pfd != null) {
FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());
String path = copyStreamToInternalStorage(context, fis, extension);
closeQuietly(fis);
try { pfd.close(); } catch (IOException ignored) { }
return path;
}
} catch (SecurityException e) {
Log.e(TAG, "openFileDescriptor SecurityException: " + e.getMessage());
} catch (IOException e) {
Log.e(TAG, "openFileDescriptor IOException: " + e.getMessage());
}
Log.e(TAG, "copyUriToInternalStorage failed for: " + sourceUri);
return null;
}
/**
* file:// Uri 或已有路径)
*/
private static String copyFileToInternalStorage(Context context, File sourceFile) {
if (sourceFile == null || !sourceFile.exists() || !sourceFile.canRead()) {
String pathForLog = "null";
if (sourceFile != null) {
try {
pathForLog = sourceFile.getCanonicalPath();
} catch (IOException e) {
pathForLog = "(path unavailable)";
}
}
Log.e(TAG, "Source file not readable: " + pathForLog);
return null;
}
String ext = ".jpg";
String name = sourceFile.getName().toLowerCase();
if (name.endsWith(".png")) ext = ".png";
else if (name.endsWith(".webp")) ext = ".webp";
else if (name.endsWith(".gif")) ext = ".gif";
FileInputStream fis = null;
try {
fis = new FileInputStream(sourceFile);
return copyStreamToInternalStorage(context, fis, ext);
} catch (IOException e) {
Log.e(TAG, "copyFileToInternalStorage failed", e);
return null;
} finally {
closeQuietly(fis);
}
}
/**
*
*/
private static String copyStreamToInternalStorage(Context context, InputStream input, String extension) {
FileOutputStream outputStream = null;
try {
File targetFile = createImageFileWithExtension(context, extension);
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[8192];
int length;
while ((length = input.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
return targetFile.getCanonicalPath();
} catch (IOException e) {
Log.e(TAG, "Failed to copy stream to internal storage", e);
return null;
} finally {
closeQuietly(outputStream);
}
}
/**
*
*/
private static void closeQuietly(OutputStream os) {
if (os != null) try { os.close(); } catch (IOException ignored) { }
}
/**
*
*/
private static void closeQuietly(InputStream is) {
if (is != null) try { is.close(); } catch (IOException ignored) { }
}
/**
* Bitmap OOM (Out Of Memory)
* @param path
* @param reqWidth
* @param reqHeight
*/
public static Bitmap getCompressedBitmap(String path, int reqWidth, int reqHeight) {
// 1. 只读取图片的尺寸信息,不加载像素到内存
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
// 2. 计算压缩比例
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 3. 真正加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
}
/**
* Bitmap (inSampleSize)使 OOM
*
* @param options inJustDecodeBounds=true Options
* @param reqWidth
* @param reqHeight
* @return 2 1248
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 计算最大的 2 的幂次,保证宽高仍大于期望宽高
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}

@ -1,30 +0,0 @@
package net.micode.notes.tool;
/**
*
*
* 退
*/
public class PrivacySpaceManager {
private static volatile boolean sInPrivacySpace = false;
/** 是否为隐私空间模式 */
public static boolean isInPrivacySpace() {
return sInPrivacySpace;
}
/** 进入隐私空间(由 Mi 管家输入「隐私空间」触发) */
public static void enterPrivacySpace() {
sInPrivacySpace = true;
}
/**
* 退
* 退
*
*/
public static void exitPrivacySpace() {
sInPrivacySpace = false;
}
}

@ -1,110 +0,0 @@
package net.micode.notes.tool;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.Image;
import android.media.ImageReader;
import android.media.projection.MediaProjection;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import java.nio.ByteBuffer;
/**
*
* <p>
* MediaProjection Bitmap
* FloatingViewManager 使便
* ImageReader VirtualDisplay
* </p>
*
* @see FloatingViewManager
* @see FloatingService
*/
public class ScreenCaptureManager {
private MediaProjection mMediaProjection;
private int mScreenWidth, mScreenHeight, mScreenDensity;
private ImageReader mImageReader; // 【关键】全局持有,不销毁
private VirtualDisplay mVirtualDisplay;
public ScreenCaptureManager(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
mScreenWidth = metrics.widthPixels;
mScreenHeight = metrics.heightPixels;
mScreenDensity = metrics.densityDpi;
// 【关键】构造时就创建好 ImageReader一直复用
// maxImages 设为 2避免 Buffer 不够
mImageReader = ImageReader.newInstance(mScreenWidth, mScreenHeight, PixelFormat.RGBA_8888, 2);
}
public void setMediaProjection(MediaProjection mp) {
this.mMediaProjection = mp;
if (mMediaProjection != null) {
// Android 14 必须注册回调,否则崩
mMediaProjection.registerCallback(new MediaProjection.Callback() {}, null);
}
}
public void capture(final OnCaptureListener listener) {
if (mMediaProjection == null || mImageReader == null) return;
// 【关键】每次只需创建 VirtualDisplay
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"ScreenCapture",
mScreenWidth, mScreenHeight, mScreenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mImageReader.getSurface(),
null, null);
// 设置单次监听
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
// 一旦收到图片,立刻移除监听,防止重复触发
reader.setOnImageAvailableListener(null, null);
// 停止投影 (释放 VirtualDisplay但不释放 ImageReader)
if (mVirtualDisplay != null) {
mVirtualDisplay.release();
mVirtualDisplay = null;
}
Bitmap bitmap = null;
Image image = null;
try {
image = reader.acquireLatestImage();
if (image != null) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * mScreenWidth;
bitmap = Bitmap.createBitmap(mScreenWidth + rowPadding / pixelStride, mScreenHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, mScreenWidth, mScreenHeight);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (image != null) image.close(); // 必须关闭 Image 释放 Buffer
final Bitmap result = bitmap;
new Handler(Looper.getMainLooper()).post(() -> {
if (listener != null) listener.onCaptured(result);
});
}
}
}, null); // 在当前线程(主线程)回调,因为这里逻辑很快,为了避免线程死锁先这样
}
public interface OnCaptureListener {
void onCaptured(Bitmap bitmap);
}
}

@ -1,142 +0,0 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.Switch;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.FloatingService;
public class InspirationFragment extends Fragment {
private ListView mListView;
private NotesListAdapter mAdapter;
private BackgroundQueryHandler mQueryHandler;
private Switch mSwitchFloating;
private static final int QUERY_TOKEN = 202;
private static final int REQUEST_CODE_OVERLAY_PERMISSION = 1001;
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1002;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_inspiration, container, false);
initViews(view);
return view;
}
private void initViews(View view) {
mListView = view.findViewById(R.id.inspiration_list);
mSwitchFloating = view.findViewById(R.id.switch_floating_ball);
mAdapter = new NotesListAdapter(getActivity());
mListView.setAdapter(mAdapter);
mQueryHandler = new BackgroundQueryHandler(getActivity().getContentResolver());
mListView.setOnItemClickListener(new android.widget.AdapterView.OnItemClickListener() {
@Override
public void onItemClick(android.widget.AdapterView<?> parent, View view, int position, long id) {
// 跳转到编辑页 (复用 NoteEditActivity)
Intent intent = new Intent(getActivity(), NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, id); // 传入便签 ID
startActivity(intent);
}
});
mSwitchFloating.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
checkAndStartService();
} else {
stopFloatingService();
}
}
});
}
private void checkAndStartService() {
// 1. 检查悬浮窗权限
if (!Settings.canDrawOverlays(getContext())) {
Toast.makeText(getActivity(), "请授予悬浮窗权限", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getActivity().getPackageName()));
startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION);
mSwitchFloating.setChecked(false);
return;
}
// 2. 申请截屏权限
MediaProjectionManager mm = (MediaProjectionManager) getActivity().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(mm.createScreenCaptureIntent(), REQUEST_CODE_SCREEN_CAPTURE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_OVERLAY_PERMISSION) {
if (Settings.canDrawOverlays(getContext())) {
checkAndStartService();
}
} else if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
if (resultCode == Activity.RESULT_OK && data != null) {
startFloatingService(resultCode, data);
} else {
mSwitchFloating.setChecked(false);
Toast.makeText(getActivity(), "未授权截屏,无法开启", Toast.LENGTH_SHORT).show();
}
}
}
private void startFloatingService(int resultCode, Intent data) {
Intent intent = new Intent(getActivity(), FloatingService.class);
intent.putExtra("resultCode", resultCode);
intent.putExtra("data", data);
getActivity().startForegroundService(intent);
}
private void stopFloatingService() {
Intent intent = new Intent(getActivity(), FloatingService.class);
getActivity().stopService(intent);
}
@Override
public void onStart() {
super.onStart();
refreshList();
}
private void refreshList() {
String selection = NoteColumns.PARENT_ID + "=?";
String[] selectionArgs = { String.valueOf(Notes.ID_INSPIRATION_FOLDER) };
mQueryHandler.startQuery(QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI,
NoteItemData.PROJECTION, selection, selectionArgs, NoteColumns.MODIFIED_DATE + " DESC");
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
public BackgroundQueryHandler(ContentResolver cr) { super(cr); }
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (token == QUERY_TOKEN) mAdapter.changeCursor(cursor);
}
}
}

@ -1,178 +0,0 @@
package net.micode.notes.ui;
/**
* Activity
* <p>
* Mi / 便 / ViewPager2 Drawer
*
* </p>
*
* @see MiStewardFragment
* @see NotesListFragment
* @see TodoFragment
*/
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.navigation.NavigationView;
import net.micode.notes.R;
public class MainActivity extends AppCompatActivity {
private ViewPager2 mViewPager;
private BottomNavigationView mBottomNav;
private DrawerLayout mDrawerLayout;
private NavigationView mNavView;
/** 便签列表页引用,由 NotesListFragment 注册,用于侧边栏菜单回调 */
private NotesListFragment mNotesListFragment;
private View mMainContentRoot;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewPager = findViewById(R.id.view_pager);
mBottomNav = findViewById(R.id.bottom_navigation);
mDrawerLayout = findViewById(R.id.drawer_layout);
mNavView = findViewById(R.id.nav_view);
mMainContentRoot = findViewById(R.id.main_content_root);
mViewPager.setAdapter(new FragmentStateAdapter(this) {
@NonNull
@Override
public Fragment createFragment(int position) {
if (position == 0) {
return new MiStewardFragment();
} else if (position == 1) {
return new NotesListFragment();
} else {
return new TodoFragment();
}
}
@Override
public int getItemCount() {
return 3;
}
});
mBottomNav.setOnItemSelectedListener(item -> {
if (item.getItemId() == R.id.nav_mi_steward) {
mViewPager.setCurrentItem(0);
return true;
} else if (item.getItemId() == R.id.nav_notes) {
mViewPager.setCurrentItem(1);
return true;
} else if (item.getItemId() == R.id.nav_todo) {
mViewPager.setCurrentItem(2);
return true;
}
return false;
});
mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (position == 0) {
mBottomNav.setSelectedItemId(R.id.nav_mi_steward);
} else if (position == 1) {
mBottomNav.setSelectedItemId(R.id.nav_notes);
} else {
mBottomNav.setSelectedItemId(R.id.nav_todo);
}
}
});
// 侧边栏菜单点击
mNavView.setNavigationItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.drawer_new_note) {
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.createNewNoteFromDrawer();
}
});
mDrawerLayout.closeDrawers();
return true;
}
if (id == R.id.drawer_new_folder) {
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.showCreateOrModifyFolderDialogFromDrawer(true);
}
});
mDrawerLayout.closeDrawers();
return true;
}
if (id == R.id.drawer_export) {
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.exportNoteToTextFromDrawer();
}
});
mDrawerLayout.closeDrawers();
return true;
}
return false;
});
}
/** 供便签页 Toolbar 菜单按钮调用,打开侧边栏 */
public void openDrawer() {
if (mDrawerLayout != null) {
mDrawerLayout.openDrawer(mNavView);
}
}
/** 由 NotesListFragment 在 onViewCreated 时注册onDestroyView 时置空 */
public void setNotesListFragment(NotesListFragment fragment) {
mNotesListFragment = fragment;
}
/**
* + 便
* MiStewardFragment
*/
public void enterPrivacySpace() {
int darkColor = ContextCompat.getColor(this, R.color.privacy_space_background);
if (mMainContentRoot != null) {
mMainContentRoot.setBackgroundColor(darkColor);
}
if (mBottomNav != null) {
mBottomNav.setBackgroundColor(darkColor);
}
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.onEnterPrivacySpace();
}
});
}
/** 退出隐私空间:恢复正常主题 */
public void exitPrivacySpace() {
if (mMainContentRoot != null) {
mMainContentRoot.setBackgroundColor(ContextCompat.getColor(this, R.color.surface_white));
}
if (mBottomNav != null) {
mBottomNav.setBackgroundColor(ContextCompat.getColor(this, R.color.surface_white));
}
}
}

@ -1,253 +0,0 @@
package net.micode.notes.ui;
/**
* Mi AI Fragment
* <p>
* Mi DeepSeek
* RAG 便 AI
*
* </p>
*
* @see ChatAdapter
* @see AIService
* @see NoteRetriever
* @see PromptBuilder
*/
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.ai.AIService;
import net.micode.notes.ai.NoteRetriever;
import net.micode.notes.ai.PromptBuilder;
import net.micode.notes.model.ChatMessage;
import net.micode.notes.tool.FloatingService;
import net.micode.notes.tool.PrivacySpaceManager;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiStewardFragment extends Fragment {
private RecyclerView mRecyclerView;
private ChatAdapter mAdapter;
private EditText mInputBox;
private View mLogoArea;
private Switch mSwitchFloating;
private CheckBox mCbUseKnowledge;
private static final int REQUEST_CODE_OVERLAY_PERMISSION = 1001;
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1002;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_mi_steward, container, false);
initViews(view);
return view;
}
@Override
public void onResume() {
super.onResume();
if (getView() != null && PrivacySpaceManager.isInPrivacySpace()) {
getView().setBackgroundColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.privacy_space_background));
}
}
private void initViews(View view) {
mRecyclerView = view.findViewById(R.id.recycler_view);
mInputBox = view.findViewById(R.id.et_input);
mLogoArea = view.findViewById(R.id.center_logo_area);
mCbUseKnowledge = view.findViewById(R.id.cb_use_knowledge);
mSwitchFloating = view.findViewById(R.id.switch_steward_floating);
Button btnSend = view.findViewById(R.id.btn_send);
ImageView btnNewChat = view.findViewById(R.id.btn_new_chat);
mAdapter = new ChatAdapter();
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerView.setAdapter(mAdapter);
// 1. 发送按钮逻辑
btnSend.setOnClickListener(v -> {
String text = mInputBox.getText().toString().trim();
if (TextUtils.isEmpty(text)) return;
mLogoArea.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
mAdapter.addMessage(new ChatMessage(text));
mInputBox.setText("");
scrollToBottom();
// 检测「隐私空间」四个字,有且仅有此四字时进入隐私空间
if ("隐私空间".equals(text)) {
enterPrivacySpaceFlow();
return;
}
performAIQuery(text);
});
// 2. 新对话按钮逻辑 (左上角)
btnNewChat.setOnClickListener(v -> {
mAdapter.clear();
mLogoArea.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
Toast.makeText(getContext(), "已开启新对话", Toast.LENGTH_SHORT).show();
});
// 3. 灵感球开关逻辑 (右上角)
mSwitchFloating.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
checkAndStartFloatingService();
} else {
Intent intent = new Intent(getActivity(), FloatingService.class);
getActivity().stopService(intent);
Toast.makeText(getActivity(), "灵感球已关闭", Toast.LENGTH_SHORT).show();
}
});
}
private void checkAndStartFloatingService() {
// 第一步:检查悬浮窗权限
if (!Settings.canDrawOverlays(getContext())) {
Toast.makeText(getActivity(), "请授予悬浮窗权限", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getActivity().getPackageName()));
startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION);
mSwitchFloating.setChecked(false); // 暂时拨回,等待授权
return;
}
// 第二步:申请截屏权限
MediaProjectionManager mm = (MediaProjectionManager) getActivity().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
if (mm != null) {
startActivityForResult(mm.createScreenCaptureIntent(), REQUEST_CODE_SCREEN_CAPTURE);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_OVERLAY_PERMISSION) {
if (Settings.canDrawOverlays(getContext())) {
checkAndStartFloatingService(); // 重新走流程,进入下一步申请截屏
}
} else if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
if (resultCode == Activity.RESULT_OK && data != null) {
// 第三步:启动服务
Intent intent = new Intent(getActivity(), FloatingService.class);
intent.putExtra("resultCode", resultCode);
intent.putExtra("data", data);
getActivity().startForegroundService(intent);
mSwitchFloating.setChecked(true);
Toast.makeText(getActivity(), "灵感球已开启", Toast.LENGTH_SHORT).show();
} else {
mSwitchFloating.setChecked(false);
Toast.makeText(getActivity(), "未授权截屏,无法开启", Toast.LENGTH_SHORT).show();
}
}
}
private void performAIQuery(String userQuery) {
ChatMessage aiMsg = new ChatMessage();
mAdapter.addMessage(aiMsg);
scrollToBottom();
boolean useRAG = mCbUseKnowledge.isChecked();
new Thread(() -> {
List<NoteRetriever.RetrievedNote> relatedNotes = null;
if (useRAG) {
relatedNotes = NoteRetriever.searchRelevantNotes(getContext(), userQuery);
}
String systemPrompt = PromptBuilder.buildSystemPrompt(relatedNotes, useRAG);
AIService.chatWithKnowledge(systemPrompt, userQuery, new AIService.AIResultCallback() {
@Override
public void onSuccess(String result) {
List<String> refs = new ArrayList<>();
Pattern pattern = Pattern.compile("\\[Ref:(.*?)\\]");
Matcher matcher = pattern.matcher(result);
while (matcher.find()) {
refs.add(matcher.group(1));
}
String cleanContent = matcher.replaceAll("").trim();
new Handler(Looper.getMainLooper()).post(() -> {
ChatMessage lastMsg = mAdapter.getLastMessage();
if (lastMsg != null && lastMsg.getType() == ChatMessage.TYPE_AI) {
lastMsg.setThinking(false);
lastMsg.setContent(cleanContent);
lastMsg.setReferences(refs);
mAdapter.updateLastMessage();
scrollToBottom();
}
});
}
@Override
public void onError(String error) {
new Handler(Looper.getMainLooper()).post(() -> {
ChatMessage lastMsg = mAdapter.getLastMessage();
if (lastMsg != null) {
lastMsg.setThinking(false);
lastMsg.setContent("AI 遇到点小麻烦:" + error);
mAdapter.updateLastMessage();
}
});
}
});
}).start();
}
private void scrollToBottom() {
mRecyclerView.scrollToPosition(mAdapter.getItemCount() - 1);
}
/**
* Mi + + 便
*/
private void enterPrivacySpaceFlow() {
// 1. Mi 管家回复
ChatMessage aiMsg = new ChatMessage();
aiMsg.setThinking(false);
aiMsg.setContent("隐私空间已开启~");
mAdapter.addMessage(aiMsg);
scrollToBottom();
// 2. 进入隐私空间模式
PrivacySpaceManager.enterPrivacySpace();
// 3. 通知 MainActivity变暗 UI 并切换到便签列表
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).enterPrivacySpace();
}
}
}

@ -1,80 +0,0 @@
package net.micode.notes.ui;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import net.micode.notes.R;
import net.micode.notes.tool.DataUtils;
/**
* 便
* Android 13+
*/
public final class NoteReminderNotifier {
private static final String TAG = "NoteReminderNotifier";
public static final String CHANNEL_ID = "note_reminder_channel";
private static final int SNIPPET_MAX_LEN = 40;
public static void show(Context context, long noteId, Uri noteUri) {
if (context == null || noteUri == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "POST_NOTIFICATIONS not granted");
return;
}
}
String snippet;
try {
snippet = DataUtils.getSnippetById(context.getContentResolver(), noteId);
if (snippet == null) snippet = "";
snippet = snippet.trim();
if (snippet.length() > SNIPPET_MAX_LEN) {
snippet = snippet.substring(0, SNIPPET_MAX_LEN) + "…";
}
} catch (Exception e) {
snippet = context.getString(R.string.note_reminder_default_text);
}
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
context.getString(R.string.note_reminder_channel_name),
NotificationManager.IMPORTANCE_HIGH);
channel.setDescription(context.getString(R.string.note_reminder_channel_desc));
nm.createNotificationChannel(channel);
}
Intent openIntent = new Intent(context, AlarmAlertActivity.class);
openIntent.setData(noteUri);
openIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pending = PendingIntent.getActivity(
context,
(int) (noteId % Integer.MAX_VALUE),
openIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.icon_app)
.setContentTitle(context.getString(R.string.note_reminder_title))
.setContentText(snippet.isEmpty() ? context.getString(R.string.note_reminder_default_text) : snippet)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setAutoCancel(true)
.setContentIntent(pending);
try {
NotificationManagerCompat.from(context).notify((int) (noteId % Integer.MAX_VALUE), builder.build());
} catch (SecurityException e) {
Log.e(TAG, "Cannot show notification: " + e.getMessage());
}
}
}

@ -1,40 +0,0 @@
package net.micode.notes.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
/**
* 便广
* 使 Receiver
*/
public class NoteReminderReceiver extends BroadcastReceiver {
private static final String TAG = "NoteReminderReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Uri data = intent.getData();
if (data == null || data.getPathSegments() == null || data.getPathSegments().size() < 2) {
Log.e(TAG, "Alarm intent has no note data");
return;
}
long noteId;
try {
noteId = Long.parseLong(data.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Invalid note id in alarm intent", e);
return;
}
NoteReminderNotifier.show(context, noteId, data);
Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
alertIntent.setData(data);
alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
try {
context.startActivity(alertIntent);
} catch (Exception e) {
Log.d(TAG, "Could not start AlarmAlertActivity: " + e.getMessage());
}
}
}

@ -1,168 +0,0 @@
package net.micode.notes.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import net.micode.notes.R;
/**
*
* + Logo + + 线
* 退 ANR使 post GlobalLayoutListener
*/
public class SplashActivity extends AppCompatActivity {
private static final int PHASE_ENTER_MS = 900;
private static final int HOLD_MS = 1100;
private static final int PHASE_EXIT_MS = 450;
/** 最大等待时间,超时则直接进入主界面,防止卡死 */
private static final int SAFETY_TIMEOUT_MS = 4000;
private View mContentRoot;
private ImageView mLogo;
private TextView mTitle;
private View mAccent;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private boolean mEnterAnimationStarted;
private boolean mGoMainCalled;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
mContentRoot = findViewById(R.id.splash_content);
mLogo = findViewById(R.id.splash_logo);
mTitle = findViewById(R.id.splash_title);
mAccent = findViewById(R.id.splash_accent);
if (mContentRoot == null || mLogo == null || mTitle == null || mAccent == null) {
goToMain();
return;
}
if (mLogo != null) {
mLogo.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
}
// 使用 post 在下一帧启动动画,避免与 GlobalLayoutListener 的 API 差异导致不触发
mContentRoot.post(this::startEnterAnimation);
// 安全超时:若动画或跳转异常,最多等待 SAFETY_TIMEOUT_MS 后强制进入主界面
mHandler.postDelayed(this::goToMain, SAFETY_TIMEOUT_MS);
}
private void startEnterAnimation() {
if (mEnterAnimationStarted || isFinishing()) return;
if (mLogo == null || mTitle == null || mAccent == null) {
goToMain();
return;
}
mEnterAnimationStarted = true;
// 1. Logo缩放 + 淡入
mLogo.setScaleX(0.82f);
mLogo.setScaleY(0.82f);
mLogo.setAlpha(0f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogo, View.SCALE_X, 0.82f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogo, View.SCALE_Y, 0.82f, 1f);
ObjectAnimator alphaLogo = ObjectAnimator.ofFloat(mLogo, View.ALPHA, 0f, 1f);
scaleX.setDuration(520);
scaleY.setDuration(520);
alphaLogo.setDuration(420);
AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
scaleX.setInterpolator(interpolator);
scaleY.setInterpolator(interpolator);
alphaLogo.setInterpolator(interpolator);
scaleX.start();
scaleY.start();
alphaLogo.start();
// 2. 标题:延迟淡入 + 上移
mTitle.setAlpha(0f);
mTitle.setTranslationY(12f);
mTitle.animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(280)
.setDuration(400)
.setInterpolator(interpolator)
.start();
// 3. 底部细线:淡入
mAccent.setAlpha(0f);
mAccent.animate()
.alpha(1f)
.setStartDelay(460)
.setDuration(320)
.setInterpolator(new LinearInterpolator())
.start();
// 4. 停留后退场并跳转
mHandler.postDelayed(this::startExitAnimation, PHASE_ENTER_MS + HOLD_MS);
}
private void startExitAnimation() {
if (isFinishing() || mGoMainCalled) return;
if (mContentRoot == null) {
goToMain();
return;
}
mContentRoot.animate()
.alpha(0f)
.setDuration(PHASE_EXIT_MS)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mContentRoot != null) {
mContentRoot.animate().setListener(null);
}
goToMain();
}
})
.start();
}
private void goToMain() {
if (mGoMainCalled) return;
mGoMainCalled = true;
mHandler.removeCallbacksAndMessages(null);
if (isFinishing()) return;
try {
startActivity(new Intent(this, MainActivity.class));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
} catch (Exception e) {
overridePendingTransition(0, 0);
}
finish();
}
@Override
protected void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
mContentRoot = null;
mLogo = null;
mTitle = null;
mAccent = null;
super.onDestroy();
}
}

@ -1,215 +0,0 @@
package net.micode.notes.ui;
/**
* /
* <p>
* DatePicker + TimePickerGeofence
* TodoReminderManager AlarmManager/Geofencing
* </p>
*
* @see TodoItem
* @see TodoReminderManager
* @see TodoDao
*/
import android.Manifest;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import android.widget.EditText;
import net.micode.notes.R;
import net.micode.notes.data.TodoDao;
import net.micode.notes.todo.TodoItem;
import net.micode.notes.todo.TodoReminderManager;
import java.util.Calendar;
public class TodoEditActivity extends AppCompatActivity {
private static final String EXTRA_TODO_ID = "todo_id";
private EditText mEtContent;
private Button mBtnAddReminder;
private TextView mTvReminderInfo;
private Button mBtnSave;
private TodoDao mTodoDao;
private TodoItem mItem;
private boolean mIsEdit;
private final ActivityResultLauncher<String[]> mLocationPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
if (Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) {
applyMockLocation();
} else {
Toast.makeText(this, "需要位置权限才能设置地点提醒", Toast.LENGTH_SHORT).show();
}
});
public static Intent newIntent(Context context) {
return new Intent(context, TodoEditActivity.class);
}
public static Intent newIntent(Context context, long todoId) {
Intent i = new Intent(context, TodoEditActivity.class);
i.putExtra(EXTRA_TODO_ID, todoId);
return i;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_todo_edit);
if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mTodoDao = new TodoDao(this);
Intent intent = getIntent();
long todoId = (intent != null) ? intent.getLongExtra(EXTRA_TODO_ID, 0) : 0;
mIsEdit = todoId > 0;
mItem = mIsEdit ? mTodoDao.getById(todoId) : new TodoItem();
if (mItem == null) mItem = new TodoItem();
mEtContent = findViewById(R.id.et_todo_content);
mBtnAddReminder = findViewById(R.id.btn_add_reminder);
mTvReminderInfo = findViewById(R.id.tv_reminder_info);
mBtnSave = findViewById(R.id.btn_save);
if (mIsEdit) mEtContent.setText(mItem.getContent());
updateReminderInfo();
mBtnAddReminder.setOnClickListener(v -> showReminderOptions());
mBtnSave.setOnClickListener(v -> save());
}
@Override
public boolean onSupportNavigateUp() {
finish();
return true;
}
private void showReminderOptions() {
String[] options = new String[]{
getString(R.string.todo_reminder_time),
getString(R.string.todo_reminder_location),
getString(R.string.todo_reminder_none)
};
new android.app.AlertDialog.Builder(this)
.setTitle(R.string.todo_add_reminder)
.setItems(options, (dialog, which) -> {
if (which == 0) pickTime();
else if (which == 1) pickLocation();
else clearReminder();
})
.show();
}
private void clearReminder() {
mItem.setReminderType(TodoItem.REMINDER_NONE);
mItem.setReminderTimestamp(0);
mItem.setLatitude(0);
mItem.setLongitude(0);
mItem.setLocationName("");
updateReminderInfo();
}
private void pickTime() {
Calendar cal = Calendar.getInstance();
if (mItem.getReminderTimestamp() > 0) cal.setTimeInMillis(mItem.getReminderTimestamp());
DatePickerDialog dateDialog = new DatePickerDialog(this,
(v, year, month, dayOfMonth) -> {
cal.set(year, month, dayOfMonth);
TimePickerDialog timeDialog = new TimePickerDialog(this,
(tv, hour, minute) -> {
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, 0);
mItem.setReminderType(TodoItem.REMINDER_TIME);
mItem.setReminderTimestamp(cal.getTimeInMillis());
mItem.setLatitude(0);
mItem.setLongitude(0);
mItem.setLocationName("");
updateReminderInfo();
},
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE),
DateFormat.is24HourFormat(this));
timeDialog.show();
},
cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH));
dateDialog.show();
}
private void pickLocation() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mLocationPermissionLauncher.launch(new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
});
} else {
mLocationPermissionLauncher.launch(new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
}
} else {
applyMockLocation();
}
}
private void applyMockLocation() {
// 模拟选点:返回虚拟坐标和地点名称(无真实地图 SDK
mItem.setReminderType(TodoItem.REMINDER_LOCATION);
mItem.setReminderTimestamp(0);
mItem.setLatitude(39.9042);
mItem.setLongitude(116.4074);
mItem.setLocationName("附近超市");
updateReminderInfo();
Toast.makeText(this, "已设置地点提醒:附近超市", Toast.LENGTH_SHORT).show();
}
private void updateReminderInfo() {
if (mItem.getReminderType() == TodoItem.REMINDER_TIME && mItem.getReminderTimestamp() > 0) {
mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
mTvReminderInfo.setText("时间提醒: " + DateFormat.format("yyyy-MM-dd HH:mm", mItem.getReminderTimestamp()));
} else if (mItem.getReminderType() == TodoItem.REMINDER_LOCATION) {
mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
mTvReminderInfo.setText("地点提醒: " + mItem.getLocationName());
} else {
mTvReminderInfo.setVisibility(android.view.View.GONE);
}
}
private void save() {
String content = mEtContent.getText() != null ? mEtContent.getText().toString().trim() : "";
if (TextUtils.isEmpty(content)) {
Toast.makeText(this, "请输入待办内容", Toast.LENGTH_SHORT).show();
return;
}
mItem.setContent(content);
if (mItem.getCreatedTime() == 0) mItem.setCreatedTime(System.currentTimeMillis());
if (mIsEdit && mItem.getId() > 0) {
mTodoDao.update(mItem);
} else {
long id = mTodoDao.insert(mItem);
mItem.setId(id);
}
TodoReminderManager.scheduleReminders(this, mItem);
setResult(RESULT_OK);
finish();
}
}

@ -1,238 +0,0 @@
package net.micode.notes.ui;
/**
* Fragment
* <p>
* /
*
*
* </p>
*
* @see TodoAdapter
* @see TodoEditActivity
* @see TodoReminderManager
*/
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import net.micode.notes.R;
import net.micode.notes.data.TodoDao;
import net.micode.notes.todo.TodoReminderManager;
import net.micode.notes.todo.ConfettiView;
import net.micode.notes.todo.TodoAdapter;
import net.micode.notes.todo.TodoItem;
import java.util.ArrayList;
import java.util.List;
public class TodoFragment extends Fragment {
private static final int REQUEST_EDIT = 2001;
private RecyclerView mRecyclerView;
private TodoAdapter mAdapter;
private FloatingActionButton mFab;
private MaterialToolbar mToolbar;
private ConfettiView mConfettiView;
private TodoDao mTodoDao;
private List<TodoItem> mUndoneList = new ArrayList<>();
private List<TodoItem> mDoneList = new ArrayList<>();
private ActionModeCallback mActionModeCallback;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_todo, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mTodoDao = new TodoDao(requireContext());
mRecyclerView = view.findViewById(R.id.recycler_todo);
mFab = view.findViewById(R.id.fab_add_todo);
mToolbar = view.findViewById(R.id.toolbar_todo);
mConfettiView = view.findViewById(R.id.confetti_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
mAdapter = new TodoAdapter();
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new TodoAdapter.OnItemClickListener() {
@Override
public void onItemClick(TodoItem item, int position) {
if (!mAdapter.isChoiceMode()) {
Intent intent = TodoEditActivity.newIntent(requireContext(), item.getId());
startActivityForResult(intent, REQUEST_EDIT);
}
}
@Override
public void onCheckChanged(TodoItem item, boolean checked) {
if (mAdapter.isChoiceMode()) return;
if (item == null || item.getId() <= 0) return;
item.setDone(checked);
mTodoDao.update(item);
refreshData();
if (checked) {
checkAndTriggerGamification();
}
}
});
mAdapter.setOnItemLongClickListener((item, position) -> {
if (!mAdapter.isChoiceMode() && mActionModeCallback == null) {
mActionModeCallback = new ActionModeCallback();
requireActivity().startActionMode(mActionModeCallback);
mAdapter.setChoiceMode(true);
mAdapter.setChecked(position, true);
mFab.setVisibility(View.GONE);
}
});
mFab.setOnClickListener(v -> {
if (getActivity() == null || !isAdded()) return;
Intent intent = new Intent(getActivity(), TodoEditActivity.class);
startActivityForResult(intent, REQUEST_EDIT);
});
if (mToolbar != null && getActivity() instanceof MainActivity) {
mToolbar.setNavigationOnClickListener(v -> ((MainActivity) getActivity()).openDrawer());
}
mConfettiView.setOnClickListener(v -> mConfettiView.setVisibility(View.GONE));
refreshData();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_EDIT && resultCode == Activity.RESULT_OK) {
refreshData();
}
}
private void refreshData() {
mUndoneList = mTodoDao.getUndone();
mDoneList = mTodoDao.getDone();
mAdapter.setData(mUndoneList, mDoneList);
}
private void checkAndTriggerGamification() {
int undoneCount = mTodoDao.getUndoneCount();
if (undoneCount == 0) {
triggerGamification();
}
}
private void triggerGamification() {
vibrate();
playSuccessSound();
showConfetti();
}
private void vibrate() {
try {
if (getContext() == null) return;
Vibrator v = (Vibrator) getContext().getSystemService(android.content.Context.VIBRATOR_SERVICE);
if (v != null && v.hasVibrator()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
v.vibrate(150);
}
}
} catch (Exception e) { /* ignore */ }
}
/** 撒花时的短促有力提示音(系统 ToneGenerator无需 raw 资源) */
private static final int SUCCESS_TONE_DURATION_MS = 120;
private void playSuccessSound() {
try {
Context ctx = getContext();
if (ctx == null) return;
ToneGenerator tone = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 85);
tone.startTone(ToneGenerator.TONE_PROP_ACK, SUCCESS_TONE_DURATION_MS);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
try {
tone.release();
} catch (Exception ignored) { }
}, SUCCESS_TONE_DURATION_MS + 80);
} catch (Exception e) { /* 无扬声器或权限时静默 */ }
}
private void showConfetti() {
if (mConfettiView != null) {
mConfettiView.post(() -> {
mConfettiView.start();
});
}
}
private class ActionModeCallback implements android.view.ActionMode.Callback {
@Override
public boolean onCreateActionMode(android.view.ActionMode mode, android.view.Menu menu) {
requireActivity().getMenuInflater().inflate(R.menu.todo_list_options, menu);
return true;
}
@Override
public boolean onPrepareActionMode(android.view.ActionMode mode, android.view.Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(android.view.ActionMode mode, android.view.MenuItem item) {
if (item.getItemId() == R.id.delete) {
List<Long> ids = mAdapter.getSelectedIds();
if (ids.isEmpty()) {
Toast.makeText(requireContext(), R.string.menu_select_none, Toast.LENGTH_SHORT).show();
return true;
}
new android.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.alert_title_delete)
.setMessage(getString(R.string.todo_delete_confirm, ids.size()))
.setPositiveButton(android.R.string.ok, (d, w) -> {
for (long id : ids) TodoReminderManager.cancelAllForTodo(requireContext(), id);
mTodoDao.deleteByIds(ids);
refreshData();
mode.finish();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
return true;
}
@Override
public void onDestroyActionMode(android.view.ActionMode mode) {
mAdapter.setChoiceMode(false);
mFab.setVisibility(View.VISIBLE);
mActionModeCallback = null;
}
}
}

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/bottom_nav_selected" android:state_checked="true" />
<item android:color="@color/bottom_nav_unselected" />
</selector>

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#CC3370FF"/> <!-- 半透明蓝色 (CC是透明度) -->
<stroke android:width="1dp" android:color="#FFFFFF"/> <!-- 白色描边 -->
</shape>

@ -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,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

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

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 退出图标,用于退出隐私空间 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<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>

@ -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="#5C5C5C"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1D1D1D"
android:pathData="M3,18h18v-2H3v2zM3,13h18v-2H3v2zM3,6v2h18V6H3z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M19.14,12.94c0.04,-0.31 0.06,-0.63 0.06,-0.94c0,-0.31 -0.02,-0.63 -0.06,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.31 4.8,11.63 4.8,12s0.02,0.69 0.06,1.06l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M12,4V1L8,5l4,4V6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>

@ -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="#60FFFFFF" />
<corners android:radius="1dp" />
</shape>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="160"
android:startColor="#0D0D0D"
android:centerColor="#151520"
android:endColor="#0D0D0D"
android:type="linear" />
</shape>

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="@color/surface_white">
<!-- 主内容区 -->
<LinearLayout
android:id="@+id/main_content_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/surface_white">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_light" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface_white"
app:itemIconTint="@color/bottom_nav_color"
app:itemTextColor="@color/bottom_nav_color"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav_menu" />
</LinearLayout>
<!-- 侧边栏 -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
android:background="@color/surface_white"
app:headerLayout="@layout/drawer_header"
app:itemIconTint="@color/drawer_icon_tint"
app:itemTextColor="@color/drawer_text_primary"
app:menu="@menu/drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

@ -1,55 +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="@drawable/splash_background"
android:fitsSystemWindows="true">
<LinearLayout
android:id="@+id/splash_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical">
<!-- Logo简约图标 -->
<ImageView
android:id="@+id/splash_logo"
android:layout_width="88dp"
android:layout_height="88dp"
android:alpha="0"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:src="@drawable/icon_app" />
<View
android:layout_width="match_parent"
android:layout_height="24dp" />
<!-- App 名称:细字重、字间距 -->
<TextView
android:id="@+id/splash_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0"
android:letterSpacing="0.15"
android:text="@string/app_name"
android:textColor="#E8E8E8"
android:textSize="22sp"
android:textStyle="normal"
android:fontFamily="sans-serif-light" />
<View
android:layout_width="match_parent"
android:layout_height="12dp" />
<!-- 副标题:极简点缀 -->
<View
android:id="@+id/splash_accent"
android:layout_width="32dp"
android:layout_height="2dp"
android:alpha="0"
android:background="@drawable/splash_accent_line" />
</LinearLayout>
</FrameLayout>

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
android:background="@color/background_light"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/et_todo_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:gravity="top"
android:hint="@string/todo_hint_content"
android:inputType="textMultiLine|textCapSentences"
android:background="@android:drawable/editbox_background"
android:padding="12dp" />
<Button
android:id="@+id/btn_add_reminder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/todo_add_reminder" />
<TextView
android:id="@+id/tv_reminder_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:visibility="gone" />
<Button
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/todo_save" />
</LinearLayout>
</ScrollView>

@ -1,27 +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:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="32dp"
android:paddingBottom="24dp"
android:background="@color/surface_white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="简约 · 高效"
android:textSize="14sp"
android:textColor="@color/text_secondary" />
</LinearLayout>

@ -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="match_parent"
android:orientation="vertical"
android:background="#FFFFFF">
<!-- 顶部控制区 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#F5F6F7"
android:paddingHorizontal="16dp"
android:elevation="2dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="💡 灵感箱"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"/>
<!-- 悬浮球开关 -->
<Switch
android:id="@+id/switch_floating_ball"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="开启桌面悬浮球 "
android:textSize="14sp"/>
</RelativeLayout>
<!-- 灵感便签列表 -->
<!-- 复用已有的 List View 逻辑 -->
<ListView
android:id="@+id/inspiration_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@null"
android:dividerHeight="0dp"/>
</LinearLayout>

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:background="@color/background_light">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface_white"
android:elevation="2dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_todo"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/todo_title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_todo"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:clipToPadding="false"
android:paddingBottom="80dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_todo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="24dp"
app:srcCompat="@drawable/ic_add_24dp"
app:tint="@android:color/white" />
<net.micode.notes.todo.ConfettiView
android:id="@+id/confetti_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:background="#40000000" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,30 +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="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:minHeight="56dp"
android:background="?attr/selectableItemBackground">
<CheckBox
android:id="@+id/cb_todo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp" />
<TextView
android:id="@+id/tv_todo_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:maxLines="3"
android:lineSpacingExtra="2dp" />
</LinearLayout>

@ -1,24 +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:background="@color/background_light">
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/divider_light" />
<TextView
android:id="@+id/tv_section_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:textStyle="bold" />
</LinearLayout>

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- 悬浮球主体:圆形背景 + 图标 -->
<ImageView
android:id="@+id/iv_floating_ball"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@android:drawable/ic_menu_edit"
android:background="@drawable/bg_floating_ball"
android:padding="12dp"
android:contentDescription="灵感球"
android:elevation="6dp"/>
<!-- elevation 提供阴影效果 -->
</FrameLayout>

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#99000000"> <!-- 半透明黑背景 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginHorizontal="32dp"
android:background="@android:drawable/dialog_holo_light_frame"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="捕捉灵感"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<EditText
android:id="@+id/et_inspiration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入此刻的想法..."
android:minLines="2"
android:gravity="top"
android:background="@android:drawable/edit_text">
<!-- 标准的自动聚焦写法 -->
<requestFocus />
</EditText>
<Button
android:id="@+id/btn_send_inspiration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="8dp"
android:text="存入灵感箱"/>
</LinearLayout>
</RelativeLayout>

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:background="@color/background_light"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 顶栏:菜单按钮 + 标题 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_notes"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface_white"
android:elevation="0dp"
app:navigationIcon="@drawable/ic_menu_24dp"
app:title="@string/notes_tab_title"
app:titleTextColor="@color/text_primary"
app:titleTextAppearance="@style/ToolbarTitleTextAppearance" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_light" />
<!-- 子文件夹时显示的标题栏(与原有逻辑兼容) -->
<TextView
android:id="@+id/tv_title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:background="@color/surface_white"
android:gravity="center_vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:singleLine="true"
android:textColor="@color/text_primary"
android:textSize="16sp" />
<ListView
android:id="@+id/notes_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:cacheColorHint="@android:color/transparent"
android:listSelector="@android:color/transparent"
android:divider="@color/divider_light"
android:dividerHeight="1dp"
android:background="@color/background_light"
android:fadingEdge="none"
android:scrollbars="none" />
</LinearLayout>
<!-- 右下角:返回上一级(仅子文件夹内显示)+ 新建便签 + 退出隐私空间(仅隐私模式下显示) -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_fab_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="84dp"
android:layout_marginBottom="88dp"
android:contentDescription="@string/fab_back_to_list"
android:visibility="gone"
app:srcCompat="@drawable/ic_arrow_back_24dp"
app:backgroundTint="@color/text_secondary"
app:tint="@color/surface_white"
app:elevation="6dp"
app:fabSize="normal"
app:maxImageSize="24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_fab_exit_privacy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="84dp"
android:layout_marginBottom="88dp"
android:contentDescription="@string/privacy_space_exit"
android:visibility="gone"
app:srcCompat="@drawable/ic_exit_privacy_24dp"
app:backgroundTint="@color/text_secondary"
app:tint="@color/surface_white"
app:elevation="6dp"
app:fabSize="normal"
app:maxImageSize="24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_fab_new_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="20dp"
android:layout_marginBottom="88dp"
android:contentDescription="@string/notelist_menu_new"
app:srcCompat="@drawable/ic_add_24dp"
app:backgroundTint="@color/accent_primary"
app:tint="@color/surface_white"
app:elevation="6dp"
app:fabSize="normal"
app:maxImageSize="24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/drawer_new_note"
android:icon="@drawable/ic_edit_note_24dp"
android:title="@string/notelist_menu_new" />
<item
android:id="@+id/drawer_new_folder"
android:icon="@drawable/ic_folder_24dp"
android:title="@string/menu_create_folder" />
<item
android:id="@+id/drawer_export"
android:icon="@drawable/ic_export_24dp"
android:title="@string/menu_export_text" />
</menu>

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_new_note"
android:title="@string/notelist_menu_new"/>
<item
android:id="@+id/menu_setting"
android:title="@string/menu_setting"/>
</menu>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/delete"
android:title="@string/todo_delete"
android:icon="@drawable/menu_delete"
android:showAsAction="always|withText" />
</menu>

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<!-- 置顶条加深色:与便签五色对应,用于列表项左侧标识置顶 -->
<color name="note_pinned_bar_yellow">#CCB8860B</color>
<color name="note_pinned_bar_blue">#CC1565C0</color>
<color name="note_pinned_bar_white">#CC616161</color>
<color name="note_pinned_bar_green">#CC2E7D32</color>
<color name="note_pinned_bar_red">#CCC62828</color>
<!-- 简约白主题 -->
<color name="surface_white">#FFFFFF</color>
<color name="background_light">#F7F8FA</color>
<color name="divider_light">#EFF1F3</color>
<color name="text_primary">#1D1D1D</color>
<color name="text_secondary">#657786</color>
<color name="accent_primary">#1DA1F2</color>
<color name="accent_primary_dark">#1A91DA</color>
<!-- 底部导航与侧边栏 -->
<color name="bottom_nav_unselected">#657786</color>
<color name="bottom_nav_selected">#1DA1F2</color>
<color name="drawer_icon_tint">#657786</color>
<color name="drawer_text_primary">#1D1D1D</color>
<!-- 隐私空间:整体变暗背景 -->
<color name="privacy_space_background">#FF1C1C1E</color>
</resources>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 代表 Context.getExternalFilesDir() 下的子目录,供 FileProvider 生成 content:// URI -->
<external-files-path name="my_images" path="images/" />
<!-- 相机拍摄照片保存在 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 即 files/Pictures/ -->
<external-files-path name="my_pictures" path="Pictures/" />
</paths>

@ -1,17 +0,0 @@
package net.micode.notes;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

@ -1,22 +0,0 @@
[versions]
agp = "8.13.1"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.13.0"
activity = "1.11.0"
constraintlayout = "2.2.1"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

Binary file not shown.

@ -1,8 +0,0 @@
#Wed Nov 19 20:15:31 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 443 B

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before

Width:  |  Height:  |  Size: 554 KiB

After

Width:  |  Height:  |  Size: 554 KiB

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

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

Loading…
Cancel
Save