Compare commits
17 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
49087887ba | 2 months ago |
|
|
314a88f7eb | 2 months ago |
|
|
56695e1657 | 2 months ago |
|
|
6898b82a65 | 2 months ago |
|
|
6e24476885 | 2 months ago |
|
|
536b49b2a4 | 2 months ago |
|
|
c1969d838c | 2 months ago |
|
|
60bb404320 | 2 months ago |
|
|
2fd73a74a4 | 2 months ago |
|
|
0f4b6f09ac | 3 months ago |
|
|
9c22f14a96 | 3 months ago |
|
|
cbea749514 | 3 months ago |
|
|
c98d3193f9 | 3 months ago |
|
|
91fe249ba5 | 3 months ago |
|
|
e118c83a80 | 3 months ago |
|
|
acc94815e1 | 3 months ago |
|
|
c69d6a66c3 | 3 months ago |
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/email.iml" filepath="$PROJECT_DIR$/.idea/email.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="NONE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="945587a8-7a4d-45ef-a458-1d4c53c38ecd" name="Changes" comment="" />
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="ClangdSettings">
|
||||||
|
<option name="formatViaClangd" value="false" />
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo"><![CDATA[{
|
||||||
|
"associatedIndex": 4
|
||||||
|
}]]></component>
|
||||||
|
<component name="ProjectId" id="3713RUBgHpQQc6mqLb73Rojp9Dc" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||||
|
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
||||||
|
"cf.first.check.clang-format": "false",
|
||||||
|
"cidr.known.project.marker": "true"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="945587a8-7a4d-45ef-a458-1d4c53c38ecd" name="Changes" comment="" />
|
||||||
|
<created>1766056742981</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1766056742981</updated>
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
#Sat Dec 13 11:54:16 CST 2025
|
||||||
|
gradle.version=8.5
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
#Sat Dec 13 11:24:51 CST 2025
|
||||||
|
java.home=E\:\\Android Studio\\jbr
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="distributionType" value="LOCAL" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleHome" value="E:\Gradle\gradle-8.5" />
|
||||||
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettings">
|
||||||
|
<option name="previewPanelProviderInfo">
|
||||||
|
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.example.emailclient'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.example.emailclient"
|
||||||
|
minSdk 24
|
||||||
|
targetSdk 34
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '17'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
|
||||||
|
// Retrofit for API calls
|
||||||
|
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||||
|
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||||
|
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
|
||||||
|
|
||||||
|
// Coroutines
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||||
|
|
||||||
|
// DataStore for preferences
|
||||||
|
implementation 'androidx.datastore:datastore-preferences:1.0.0'
|
||||||
|
|
||||||
|
// Room for local database
|
||||||
|
implementation 'androidx.room:room-runtime:2.6.1'
|
||||||
|
implementation 'androidx.room:room-ktx:2.6.1'
|
||||||
|
kapt 'androidx.room:room-compiler:2.6.1'
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Retrofit
|
||||||
|
-keepattributes Signature
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
-keep class retrofit2.** { *; }
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@retrofit2.http.* <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gson
|
||||||
|
-keep class com.example.emailclient.data.** { *; }
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# OkHttp
|
||||||
|
-dontwarn okhttp3.**
|
||||||
|
-dontwarn okio.**
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".EmailApp"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.EmailClient"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.SplashActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.EmailClient.Splash">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".ui.LoginActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.RegisterActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.MainActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.ComposeActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.EmailDetailActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.SettingsActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.ContactsActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.ContactDetailActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.AdminActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.StarredActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.StarredEmailDetailActivity" android:exported="false" />
|
||||||
|
<activity android:name=".ui.LocalEmailDetailActivity" android:exported="false" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.example.emailclient
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.example.emailclient.data.PreferencesManager
|
||||||
|
import com.example.emailclient.data.local.EmailRepository
|
||||||
|
|
||||||
|
class EmailApp : Application() {
|
||||||
|
lateinit var preferencesManager: PreferencesManager
|
||||||
|
private set
|
||||||
|
|
||||||
|
lateinit var emailRepository: EmailRepository
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
|
preferencesManager = PreferencesManager(this)
|
||||||
|
emailRepository = EmailRepository(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
lateinit var instance: EmailApp
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
package com.example.emailclient.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
// API响应
|
||||||
|
data class ApiResponse<T>(
|
||||||
|
val code: Int,
|
||||||
|
val message: String,
|
||||||
|
val data: T?
|
||||||
|
)
|
||||||
|
|
||||||
|
// 用户
|
||||||
|
data class User(
|
||||||
|
val id: Int,
|
||||||
|
val username: String,
|
||||||
|
val email: String,
|
||||||
|
@SerializedName("is_admin") val isAdmin: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LoginResponse(
|
||||||
|
val token: String,
|
||||||
|
val user: User
|
||||||
|
)
|
||||||
|
|
||||||
|
// 联系人
|
||||||
|
data class Contact(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val email: String,
|
||||||
|
val note: String,
|
||||||
|
@SerializedName("created_at") val createdAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ContactListResponse(
|
||||||
|
val contacts: List<Contact>,
|
||||||
|
val total: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
// 联系人邮件
|
||||||
|
data class ContactEmail(
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("sender_address") val senderAddress: String,
|
||||||
|
@SerializedName("recipient_address") val recipientAddress: String,
|
||||||
|
val subject: String,
|
||||||
|
val body: String,
|
||||||
|
@SerializedName("created_at") val createdAt: String,
|
||||||
|
@SerializedName("is_read") val isRead: Boolean,
|
||||||
|
@SerializedName("is_sent") val isSent: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ContactEmailsResponse(
|
||||||
|
val contact: Contact,
|
||||||
|
val emails: List<ContactEmail>,
|
||||||
|
val total: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
// 请求体
|
||||||
|
data class LoginRequest(val username: String, val password: String)
|
||||||
|
data class RegisterRequest(val username: String, val email: String, val password: String)
|
||||||
|
data class AddContactRequest(val name: String, val email: String, val note: String = "")
|
||||||
|
data class UpdateContactRequest(val name: String, val note: String = "")
|
||||||
|
data class AutoAddContactRequest(val email: String, val name: String? = null)
|
||||||
|
data class ChangePasswordRequest(
|
||||||
|
@SerializedName("old_password") val oldPassword: String,
|
||||||
|
@SerializedName("new_password") val newPassword: String
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== 管理员相关 ====================
|
||||||
|
|
||||||
|
data class AdminDashboard(
|
||||||
|
@SerializedName("user_count") val userCount: Int,
|
||||||
|
@SerializedName("email_count") val emailCount: Int,
|
||||||
|
@SerializedName("smtp_port") val smtpPort: Int,
|
||||||
|
@SerializedName("pop3_port") val pop3Port: Int,
|
||||||
|
val domain: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AdminUser(
|
||||||
|
val id: Int,
|
||||||
|
val username: String,
|
||||||
|
val email: String,
|
||||||
|
@SerializedName("is_admin") val isAdmin: Boolean,
|
||||||
|
@SerializedName("is_active") val isActive: Boolean,
|
||||||
|
@SerializedName("created_at") val createdAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AdminUserListResponse(
|
||||||
|
val users: List<AdminUser>,
|
||||||
|
val total: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AddUserRequest(
|
||||||
|
val username: String,
|
||||||
|
val email: String,
|
||||||
|
val password: String,
|
||||||
|
@SerializedName("is_admin") val isAdmin: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ToggleUserResponse(
|
||||||
|
@SerializedName("is_active") val isActive: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EmailFilter(
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("filter_type") val filterType: String,
|
||||||
|
val value: String,
|
||||||
|
val action: String,
|
||||||
|
@SerializedName("created_at") val createdAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FilterListResponse(
|
||||||
|
val filters: List<EmailFilter>,
|
||||||
|
val total: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AddFilterRequest(
|
||||||
|
@SerializedName("filter_type") val filterType: String,
|
||||||
|
val value: String,
|
||||||
|
val action: String = "block"
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LogEntry(
|
||||||
|
val id: Int,
|
||||||
|
val level: String,
|
||||||
|
val message: String,
|
||||||
|
val source: String?,
|
||||||
|
@SerializedName("ip_address") val ipAddress: String?,
|
||||||
|
@SerializedName("created_at") val createdAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LogListResponse(
|
||||||
|
val logs: List<LogEntry>,
|
||||||
|
val total: Int,
|
||||||
|
val page: Int,
|
||||||
|
val pages: Int,
|
||||||
|
@SerializedName("has_next") val hasNext: Boolean,
|
||||||
|
@SerializedName("has_prev") val hasPrev: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BroadcastRequest(
|
||||||
|
val subject: String,
|
||||||
|
val body: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BroadcastResponse(
|
||||||
|
@SerializedName("sent_count") val sentCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== 星标邮件相关 ====================
|
||||||
|
|
||||||
|
data class StarredEmail(
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("sender_address") val senderAddress: String,
|
||||||
|
@SerializedName("recipient_address") val recipientAddress: String,
|
||||||
|
val subject: String,
|
||||||
|
val body: String,
|
||||||
|
@SerializedName("created_at") val createdAt: String,
|
||||||
|
@SerializedName("is_read") val isRead: Boolean,
|
||||||
|
@SerializedName("is_starred") val isStarred: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StarredEmailListResponse(
|
||||||
|
val emails: List<StarredEmail>,
|
||||||
|
val total: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ToggleStarResponse(
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("is_starred") val isStarred: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StarByInfoRequest(
|
||||||
|
val sender: String,
|
||||||
|
val subject: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CheckStarredResponse(
|
||||||
|
val id: Int? = null,
|
||||||
|
@SerializedName("is_starred") val isStarred: Boolean,
|
||||||
|
val found: Boolean
|
||||||
|
)
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package com.example.emailclient.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
|
|
||||||
|
class PreferencesManager(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TOKEN = stringPreferencesKey("token")
|
||||||
|
val USERNAME = stringPreferencesKey("username")
|
||||||
|
val EMAIL = stringPreferencesKey("email")
|
||||||
|
val IS_ADMIN = stringPreferencesKey("is_admin")
|
||||||
|
val SERVER_HOST = stringPreferencesKey("server_host")
|
||||||
|
val SMTP_PORT = stringPreferencesKey("smtp_port")
|
||||||
|
val POP3_PORT = stringPreferencesKey("pop3_port")
|
||||||
|
val API_PORT = stringPreferencesKey("api_port")
|
||||||
|
}
|
||||||
|
|
||||||
|
val token: Flow<String?> = context.dataStore.data.map { it[TOKEN] }
|
||||||
|
val username: Flow<String?> = context.dataStore.data.map { it[USERNAME] }
|
||||||
|
val email: Flow<String?> = context.dataStore.data.map { it[EMAIL] }
|
||||||
|
val isAdmin: Flow<Boolean> = context.dataStore.data.map { it[IS_ADMIN] == "true" }
|
||||||
|
|
||||||
|
fun getTokenSync(): String? = runBlocking { token.first() }
|
||||||
|
fun getUsernameSync(): String? = runBlocking { username.first() }
|
||||||
|
fun getEmailSync(): String? = runBlocking { email.first() }
|
||||||
|
fun isAdminSync(): Boolean = runBlocking { isAdmin.first() }
|
||||||
|
|
||||||
|
fun getServerHost(): String = runBlocking {
|
||||||
|
context.dataStore.data.first()[SERVER_HOST] ?: "113.45.148.222"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSmtpPort(): Int = runBlocking {
|
||||||
|
context.dataStore.data.first()[SMTP_PORT]?.toIntOrNull() ?: 25
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPop3Port(): Int = runBlocking {
|
||||||
|
context.dataStore.data.first()[POP3_PORT]?.toIntOrNull() ?: 110
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApiPort(): Int = runBlocking {
|
||||||
|
context.dataStore.data.first()[API_PORT]?.toIntOrNull() ?: 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveUserInfo(token: String, username: String, email: String, isAdmin: Boolean = false) {
|
||||||
|
context.dataStore.edit {
|
||||||
|
it[TOKEN] = token
|
||||||
|
it[USERNAME] = username
|
||||||
|
it[EMAIL] = email
|
||||||
|
it[IS_ADMIN] = isAdmin.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveServerConfig(host: String, smtpPort: Int, pop3Port: Int, apiPort: Int) {
|
||||||
|
context.dataStore.edit {
|
||||||
|
it[SERVER_HOST] = host
|
||||||
|
it[SMTP_PORT] = smtpPort.toString()
|
||||||
|
it[POP3_PORT] = pop3Port.toString()
|
||||||
|
it[API_PORT] = apiPort.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearUserInfo() {
|
||||||
|
context.dataStore.edit {
|
||||||
|
it.remove(TOKEN)
|
||||||
|
it.remove(USERNAME)
|
||||||
|
it.remove(EMAIL)
|
||||||
|
it.remove(IS_ADMIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.example.emailclient.data.local
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
|
@Database(entities = [LocalEmail::class], version = 1, exportSchema = false)
|
||||||
|
abstract class EmailDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun emailDao(): EmailDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: EmailDatabase? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): EmailDatabase {
|
||||||
|
return INSTANCE ?: synchronized(this) {
|
||||||
|
val instance = Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
EmailDatabase::class.java,
|
||||||
|
"email_database"
|
||||||
|
).build()
|
||||||
|
INSTANCE = instance
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
package com.example.emailclient.data.local
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件仓库 - 统一管理本地邮件数据
|
||||||
|
*/
|
||||||
|
class EmailRepository(context: Context) {
|
||||||
|
|
||||||
|
private val emailDao = EmailDatabase.getInstance(context).emailDao()
|
||||||
|
|
||||||
|
// 收件箱
|
||||||
|
suspend fun getInboxEmails() = emailDao.getInboxEmails()
|
||||||
|
|
||||||
|
// 发件箱
|
||||||
|
suspend fun getSentEmails() = emailDao.getSentEmails()
|
||||||
|
|
||||||
|
// 草稿箱
|
||||||
|
suspend fun getDraftEmails() = emailDao.getDraftEmails()
|
||||||
|
|
||||||
|
// 星标邮件
|
||||||
|
suspend fun getStarredEmails() = emailDao.getStarredEmails()
|
||||||
|
|
||||||
|
// 获取单封邮件
|
||||||
|
suspend fun getEmailById(id: Long) = emailDao.getEmailById(id)
|
||||||
|
|
||||||
|
// 保存收到的邮件
|
||||||
|
suspend fun saveInboxEmail(from: String, to: String, subject: String, body: String): Long {
|
||||||
|
val email = LocalEmail(
|
||||||
|
senderAddress = from,
|
||||||
|
recipientAddress = to,
|
||||||
|
subject = subject,
|
||||||
|
body = body,
|
||||||
|
folderType = LocalEmail.FOLDER_INBOX,
|
||||||
|
isSent = false
|
||||||
|
)
|
||||||
|
return emailDao.insert(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存已发送邮件
|
||||||
|
suspend fun saveSentEmail(from: String, to: String, subject: String, body: String): Long {
|
||||||
|
val email = LocalEmail(
|
||||||
|
senderAddress = from,
|
||||||
|
recipientAddress = to,
|
||||||
|
subject = subject,
|
||||||
|
body = body,
|
||||||
|
folderType = LocalEmail.FOLDER_SENT,
|
||||||
|
isSent = true,
|
||||||
|
isRead = true
|
||||||
|
)
|
||||||
|
return emailDao.insert(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存草稿
|
||||||
|
suspend fun saveDraft(from: String, to: String, subject: String, body: String): Long {
|
||||||
|
val email = LocalEmail(
|
||||||
|
senderAddress = from,
|
||||||
|
recipientAddress = to,
|
||||||
|
subject = subject,
|
||||||
|
body = body,
|
||||||
|
folderType = LocalEmail.FOLDER_DRAFT,
|
||||||
|
isDraft = true
|
||||||
|
)
|
||||||
|
return emailDao.insert(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新草稿
|
||||||
|
suspend fun updateDraft(id: Long, to: String, subject: String, body: String) {
|
||||||
|
val email = emailDao.getEmailById(id) ?: return
|
||||||
|
emailDao.update(email.copy(
|
||||||
|
recipientAddress = to,
|
||||||
|
subject = subject,
|
||||||
|
body = body
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除邮件
|
||||||
|
suspend fun deleteEmail(id: Long) = emailDao.deleteById(id)
|
||||||
|
|
||||||
|
// 切换星标
|
||||||
|
suspend fun toggleStar(id: Long) = emailDao.toggleStar(id)
|
||||||
|
|
||||||
|
// 标记已读
|
||||||
|
suspend fun markAsRead(id: Long) = emailDao.markAsRead(id)
|
||||||
|
|
||||||
|
// 发送草稿(移动到发件箱)
|
||||||
|
suspend fun sendDraft(id: Long) = emailDao.moveDraftToSent(id)
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
suspend fun searchEmails(query: String) = emailDao.searchEmails(query)
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package com.example.emailclient.data.local
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地邮件实体
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "emails")
|
||||||
|
data class LocalEmail(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
val id: Long = 0,
|
||||||
|
@ColumnInfo(name = "sender_address")
|
||||||
|
val senderAddress: String,
|
||||||
|
@ColumnInfo(name = "recipient_address")
|
||||||
|
val recipientAddress: String,
|
||||||
|
val subject: String,
|
||||||
|
val body: String,
|
||||||
|
@ColumnInfo(name = "created_at")
|
||||||
|
val createdAt: Long = System.currentTimeMillis(),
|
||||||
|
@ColumnInfo(name = "is_read")
|
||||||
|
val isRead: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_starred")
|
||||||
|
val isStarred: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_draft")
|
||||||
|
val isDraft: Boolean = false,
|
||||||
|
@ColumnInfo(name = "is_sent")
|
||||||
|
val isSent: Boolean = false, // true=发件箱, false=收件箱
|
||||||
|
@ColumnInfo(name = "folder_type")
|
||||||
|
val folderType: Int = FOLDER_INBOX // 0=收件箱, 1=发件箱, 2=草稿箱
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val FOLDER_INBOX = 0
|
||||||
|
const val FOLDER_SENT = 1
|
||||||
|
const val FOLDER_DRAFT = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件DAO
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface EmailDao {
|
||||||
|
// 收件箱
|
||||||
|
@Query("SELECT * FROM emails WHERE folder_type = 0 ORDER BY created_at DESC")
|
||||||
|
suspend fun getInboxEmails(): List<LocalEmail>
|
||||||
|
|
||||||
|
// 发件箱
|
||||||
|
@Query("SELECT * FROM emails WHERE folder_type = 1 ORDER BY created_at DESC")
|
||||||
|
suspend fun getSentEmails(): List<LocalEmail>
|
||||||
|
|
||||||
|
// 草稿箱
|
||||||
|
@Query("SELECT * FROM emails WHERE folder_type = 2 ORDER BY created_at DESC")
|
||||||
|
suspend fun getDraftEmails(): List<LocalEmail>
|
||||||
|
|
||||||
|
// 星标邮件
|
||||||
|
@Query("SELECT * FROM emails WHERE is_starred = 1 ORDER BY created_at DESC")
|
||||||
|
suspend fun getStarredEmails(): List<LocalEmail>
|
||||||
|
|
||||||
|
// 根据ID获取邮件
|
||||||
|
@Query("SELECT * FROM emails WHERE id = :id")
|
||||||
|
suspend fun getEmailById(id: Long): LocalEmail?
|
||||||
|
|
||||||
|
// 插入邮件
|
||||||
|
@Insert
|
||||||
|
suspend fun insert(email: LocalEmail): Long
|
||||||
|
|
||||||
|
// 更新邮件
|
||||||
|
@Update
|
||||||
|
suspend fun update(email: LocalEmail)
|
||||||
|
|
||||||
|
// 删除邮件
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(email: LocalEmail)
|
||||||
|
|
||||||
|
// 根据ID删除
|
||||||
|
@Query("DELETE FROM emails WHERE id = :id")
|
||||||
|
suspend fun deleteById(id: Long)
|
||||||
|
|
||||||
|
// 切换星标状态
|
||||||
|
@Query("UPDATE emails SET is_starred = NOT is_starred WHERE id = :id")
|
||||||
|
suspend fun toggleStar(id: Long)
|
||||||
|
|
||||||
|
// 标记为已读
|
||||||
|
@Query("UPDATE emails SET is_read = 1 WHERE id = :id")
|
||||||
|
suspend fun markAsRead(id: Long)
|
||||||
|
|
||||||
|
// 将草稿移动到发件箱
|
||||||
|
@Query("UPDATE emails SET folder_type = 1, is_draft = 0, is_sent = 1 WHERE id = :id")
|
||||||
|
suspend fun moveDraftToSent(id: Long)
|
||||||
|
|
||||||
|
// 搜索邮件
|
||||||
|
@Query("SELECT * FROM emails WHERE subject LIKE '%' || :query || '%' OR body LIKE '%' || :query || '%' ORDER BY created_at DESC")
|
||||||
|
suspend fun searchEmails(query: String): List<LocalEmail>
|
||||||
|
}
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
package com.example.emailclient.network
|
||||||
|
|
||||||
|
import com.example.emailclient.data.*
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API接口 - 仅用于认证和通讯录
|
||||||
|
* 邮件收发通过SMTP/POP3协议实现
|
||||||
|
*/
|
||||||
|
interface ApiService {
|
||||||
|
|
||||||
|
// ==================== 认证 ====================
|
||||||
|
|
||||||
|
@POST("api/auth/login")
|
||||||
|
suspend fun login(@Body request: LoginRequest): Response<ApiResponse<LoginResponse>>
|
||||||
|
|
||||||
|
@POST("api/auth/register")
|
||||||
|
suspend fun register(@Body request: RegisterRequest): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@POST("api/auth/logout")
|
||||||
|
suspend fun logout(@Header("Authorization") token: String): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@GET("api/auth/profile")
|
||||||
|
suspend fun getProfile(@Header("Authorization") token: String): Response<ApiResponse<User>>
|
||||||
|
|
||||||
|
@PUT("api/auth/password")
|
||||||
|
suspend fun changePassword(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: ChangePasswordRequest
|
||||||
|
): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
// ==================== 通讯录 ====================
|
||||||
|
|
||||||
|
@GET("api/contacts")
|
||||||
|
suspend fun getContacts(@Header("Authorization") token: String): Response<ApiResponse<ContactListResponse>>
|
||||||
|
|
||||||
|
@POST("api/contacts")
|
||||||
|
suspend fun addContact(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: AddContactRequest
|
||||||
|
): Response<ApiResponse<Contact>>
|
||||||
|
|
||||||
|
@DELETE("api/contacts/{id}")
|
||||||
|
suspend fun deleteContact(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@PUT("api/contacts/{id}")
|
||||||
|
suspend fun updateContact(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Path("id") id: Int,
|
||||||
|
@Body request: UpdateContactRequest
|
||||||
|
): Response<ApiResponse<Contact>>
|
||||||
|
|
||||||
|
@GET("api/contacts/{id}/emails")
|
||||||
|
suspend fun getContactEmails(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<ApiResponse<ContactEmailsResponse>>
|
||||||
|
|
||||||
|
@POST("api/contacts/auto-add")
|
||||||
|
suspend fun autoAddContact(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: AutoAddContactRequest
|
||||||
|
): Response<ApiResponse<Contact>>
|
||||||
|
|
||||||
|
@GET("api/contacts/search")
|
||||||
|
suspend fun searchContacts(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Query("q") query: String
|
||||||
|
): Response<ApiResponse<ContactListResponse>>
|
||||||
|
|
||||||
|
// ==================== 管理员接口 ====================
|
||||||
|
|
||||||
|
@GET("api/admin/dashboard")
|
||||||
|
suspend fun getAdminDashboard(
|
||||||
|
@Header("Authorization") token: String
|
||||||
|
): Response<ApiResponse<AdminDashboard>>
|
||||||
|
|
||||||
|
@GET("api/admin/users")
|
||||||
|
suspend fun getAdminUsers(
|
||||||
|
@Header("Authorization") token: String
|
||||||
|
): Response<ApiResponse<AdminUserListResponse>>
|
||||||
|
|
||||||
|
@POST("api/admin/users")
|
||||||
|
suspend fun addAdminUser(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: AddUserRequest
|
||||||
|
): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@POST("api/admin/users/{id}/toggle")
|
||||||
|
suspend fun toggleUser(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<ApiResponse<ToggleUserResponse>>
|
||||||
|
|
||||||
|
@DELETE("api/admin/users/{id}")
|
||||||
|
suspend fun deleteUser(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@GET("api/admin/filters")
|
||||||
|
suspend fun getFilters(
|
||||||
|
@Header("Authorization") token: String
|
||||||
|
): Response<ApiResponse<FilterListResponse>>
|
||||||
|
|
||||||
|
@POST("api/admin/filters")
|
||||||
|
suspend fun addFilter(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: AddFilterRequest
|
||||||
|
): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@DELETE("api/admin/filters/{id}")
|
||||||
|
suspend fun deleteFilter(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@GET("api/admin/logs")
|
||||||
|
suspend fun getLogs(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("per_page") perPage: Int = 50
|
||||||
|
): Response<ApiResponse<LogListResponse>>
|
||||||
|
|
||||||
|
@POST("api/admin/logs/clear")
|
||||||
|
suspend fun clearLogs(
|
||||||
|
@Header("Authorization") token: String
|
||||||
|
): Response<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@POST("api/admin/broadcast")
|
||||||
|
suspend fun broadcast(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: BroadcastRequest
|
||||||
|
): Response<ApiResponse<BroadcastResponse>>
|
||||||
|
|
||||||
|
// ==================== 星标邮件接口 ====================
|
||||||
|
|
||||||
|
@GET("api/emails/starred")
|
||||||
|
suspend fun getStarredEmails(
|
||||||
|
@Header("Authorization") token: String
|
||||||
|
): Response<ApiResponse<StarredEmailListResponse>>
|
||||||
|
|
||||||
|
@POST("api/emails/{id}/star")
|
||||||
|
suspend fun toggleStar(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Path("id") id: Int
|
||||||
|
): Response<ApiResponse<ToggleStarResponse>>
|
||||||
|
|
||||||
|
@POST("api/emails/star-by-info")
|
||||||
|
suspend fun toggleStarByInfo(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: StarByInfoRequest
|
||||||
|
): Response<ApiResponse<ToggleStarResponse>>
|
||||||
|
|
||||||
|
@POST("api/emails/check-starred")
|
||||||
|
suspend fun checkStarred(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body request: StarByInfoRequest
|
||||||
|
): Response<ApiResponse<CheckStarredResponse>>
|
||||||
|
}
|
||||||
@ -0,0 +1,211 @@
|
|||||||
|
package com.example.emailclient.network
|
||||||
|
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.net.Socket
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class Pop3Email(
|
||||||
|
val id: Int,
|
||||||
|
val uid: String,
|
||||||
|
val from: String,
|
||||||
|
val to: String,
|
||||||
|
val subject: String,
|
||||||
|
val body: String,
|
||||||
|
val rawData: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class Pop3Client {
|
||||||
|
|
||||||
|
private val prefs = EmailApp.instance.preferencesManager
|
||||||
|
|
||||||
|
suspend fun fetchEmails(username: String, password: String): Result<List<Pop3Email>> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
var socket: Socket? = null
|
||||||
|
try {
|
||||||
|
val host = prefs.getServerHost()
|
||||||
|
val port = prefs.getPop3Port()
|
||||||
|
|
||||||
|
socket = Socket(host, port)
|
||||||
|
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
||||||
|
val writer = PrintWriter(socket.getOutputStream(), true)
|
||||||
|
|
||||||
|
// 读取欢迎消息
|
||||||
|
var response = reader.readLine()
|
||||||
|
if (!response.startsWith("+OK")) {
|
||||||
|
return@withContext Result.failure(Exception("连接失败: $response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// USER
|
||||||
|
writer.println("USER $username")
|
||||||
|
response = reader.readLine()
|
||||||
|
if (!response.startsWith("+OK")) {
|
||||||
|
return@withContext Result.failure(Exception("用户名错误: $response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASS
|
||||||
|
writer.println("PASS $password")
|
||||||
|
response = reader.readLine()
|
||||||
|
if (!response.startsWith("+OK")) {
|
||||||
|
return@withContext Result.failure(Exception("密码错误: $response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// STAT获取邮件数量
|
||||||
|
writer.println("STAT")
|
||||||
|
response = reader.readLine()
|
||||||
|
if (!response.startsWith("+OK")) {
|
||||||
|
return@withContext Result.failure(Exception("STAT失败: $response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val parts = response.split(" ")
|
||||||
|
val count = if (parts.size >= 2) parts[1].toIntOrNull() ?: 0 else 0
|
||||||
|
|
||||||
|
val emails = mutableListOf<Pop3Email>()
|
||||||
|
|
||||||
|
// 获取每封邮件的UID
|
||||||
|
writer.println("UIDL")
|
||||||
|
response = reader.readLine()
|
||||||
|
if (!response.startsWith("+OK")) {
|
||||||
|
return@withContext Result.failure(Exception("UIDL失败: $response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val uidMap = mutableMapOf<Int, String>()
|
||||||
|
while (true) {
|
||||||
|
val line = reader.readLine()
|
||||||
|
if (line == ".") break
|
||||||
|
val uidParts = line.split(" ")
|
||||||
|
if (uidParts.size >= 2) {
|
||||||
|
uidMap[uidParts[0].toIntOrNull() ?: 0] = uidParts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每封邮件内容
|
||||||
|
for (i in 1..count) {
|
||||||
|
writer.println("RETR $i")
|
||||||
|
response = reader.readLine()
|
||||||
|
if (!response.startsWith("+OK")) continue
|
||||||
|
|
||||||
|
val emailContent = StringBuilder()
|
||||||
|
while (true) {
|
||||||
|
val line = reader.readLine()
|
||||||
|
if (line == ".") break
|
||||||
|
emailContent.appendLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rawData = emailContent.toString()
|
||||||
|
val email = parseEmail(i, uidMap[i] ?: "", rawData)
|
||||||
|
emails.add(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QUIT
|
||||||
|
writer.println("QUIT")
|
||||||
|
reader.readLine()
|
||||||
|
|
||||||
|
Result.success(emails.reversed()) // 最新的在前
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
socket?.close()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteEmail(username: String, password: String, messageNumber: Int): Result<Unit> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
var socket: Socket? = null
|
||||||
|
try {
|
||||||
|
val host = prefs.getServerHost()
|
||||||
|
val port = prefs.getPop3Port()
|
||||||
|
|
||||||
|
socket = Socket(host, port)
|
||||||
|
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
||||||
|
val writer = PrintWriter(socket.getOutputStream(), true)
|
||||||
|
|
||||||
|
reader.readLine() // 欢迎消息
|
||||||
|
|
||||||
|
writer.println("USER $username")
|
||||||
|
reader.readLine()
|
||||||
|
|
||||||
|
writer.println("PASS $password")
|
||||||
|
val response = reader.readLine()
|
||||||
|
if (!response.startsWith("+OK")) {
|
||||||
|
return@withContext Result.failure(Exception("认证失败"))
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.println("DELE $messageNumber")
|
||||||
|
val deleResponse = reader.readLine()
|
||||||
|
|
||||||
|
writer.println("QUIT")
|
||||||
|
reader.readLine()
|
||||||
|
|
||||||
|
if (deleResponse.startsWith("+OK")) {
|
||||||
|
Result.success(Unit)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("删除失败: $deleResponse"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
socket?.close()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseEmail(id: Int, uid: String, rawData: String): Pop3Email {
|
||||||
|
val lines = rawData.lines()
|
||||||
|
var from = ""
|
||||||
|
var to = ""
|
||||||
|
var subject = ""
|
||||||
|
var headerEnd = 0
|
||||||
|
|
||||||
|
for ((index, line) in lines.withIndex()) {
|
||||||
|
when {
|
||||||
|
line.isEmpty() -> {
|
||||||
|
headerEnd = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
line.lowercase().startsWith("from:") -> {
|
||||||
|
from = line.substringAfter(":").trim()
|
||||||
|
// 提取邮箱地址
|
||||||
|
val match = Regex("<(.+?)>").find(from)
|
||||||
|
if (match != null) {
|
||||||
|
from = match.groupValues[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.lowercase().startsWith("to:") -> {
|
||||||
|
to = line.substringAfter(":").trim()
|
||||||
|
val match = Regex("<(.+?)>").find(to)
|
||||||
|
if (match != null) {
|
||||||
|
to = match.groupValues[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.lowercase().startsWith("subject:") -> {
|
||||||
|
subject = line.substringAfter(":").trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = if (headerEnd > 0 && headerEnd < lines.size - 1) {
|
||||||
|
lines.subList(headerEnd + 1, lines.size).joinToString("\n")
|
||||||
|
} else {
|
||||||
|
rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pop3Email(
|
||||||
|
id = id,
|
||||||
|
uid = uid,
|
||||||
|
from = from,
|
||||||
|
to = to,
|
||||||
|
subject = subject.ifEmpty { "(无主题)" },
|
||||||
|
body = body.trim(),
|
||||||
|
rawData = rawData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package com.example.emailclient.network
|
||||||
|
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object RetrofitClient {
|
||||||
|
|
||||||
|
private var retrofit: Retrofit? = null
|
||||||
|
private var currentBaseUrl: String? = null
|
||||||
|
|
||||||
|
private fun getBaseUrl(): String {
|
||||||
|
val prefs = EmailApp.instance.preferencesManager
|
||||||
|
val host = prefs.getServerHost()
|
||||||
|
val port = prefs.getApiPort()
|
||||||
|
return "http://$host:$port/"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApiService(): ApiService {
|
||||||
|
val baseUrl = getBaseUrl()
|
||||||
|
|
||||||
|
if (retrofit == null || currentBaseUrl != baseUrl) {
|
||||||
|
val logging = HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(logging)
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.MILLISECONDS))
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.header("Connection", "close")
|
||||||
|
.build()
|
||||||
|
chain.proceed(request)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
currentBaseUrl = baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return retrofit!!.create(ApiService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetClient() {
|
||||||
|
retrofit = null
|
||||||
|
currentBaseUrl = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import com.example.emailclient.data.UpdateContactRequest
|
||||||
|
import com.example.emailclient.databinding.ActivityContactDetailBinding
|
||||||
|
import com.example.emailclient.network.RetrofitClient
|
||||||
|
import com.example.emailclient.ui.adapter.ContactEmailAdapter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ContactDetailActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityContactDetailBinding
|
||||||
|
private lateinit var adapter: ContactEmailAdapter
|
||||||
|
private val prefs by lazy { EmailApp.instance.preferencesManager }
|
||||||
|
|
||||||
|
private var contactId: Int = 0
|
||||||
|
private var contactEmail: String = ""
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityContactDetailBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
|
||||||
|
contactId = intent.getIntExtra("contact_id", 0)
|
||||||
|
val name = intent.getStringExtra("contact_name") ?: ""
|
||||||
|
contactEmail = intent.getStringExtra("contact_email") ?: ""
|
||||||
|
val note = intent.getStringExtra("contact_note") ?: ""
|
||||||
|
|
||||||
|
// 设置初始值
|
||||||
|
binding.etName.setText(name)
|
||||||
|
binding.tvEmail.text = "邮箱: $contactEmail"
|
||||||
|
binding.etNote.setText(note)
|
||||||
|
binding.toolbar.title = name
|
||||||
|
|
||||||
|
setupRecyclerView()
|
||||||
|
setupListeners()
|
||||||
|
loadContactEmails()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
adapter = ContactEmailAdapter { email ->
|
||||||
|
val intent = Intent(this, EmailDetailActivity::class.java).apply {
|
||||||
|
putExtra("from", email.senderAddress)
|
||||||
|
putExtra("to", email.recipientAddress)
|
||||||
|
putExtra("subject", email.subject)
|
||||||
|
putExtra("body", email.body)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupListeners() {
|
||||||
|
binding.btnSave.setOnClickListener { saveContact() }
|
||||||
|
binding.fabCompose.setOnClickListener {
|
||||||
|
val intent = Intent(this, ComposeActivity::class.java).apply {
|
||||||
|
putExtra("reply_to", contactEmail)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveContact() {
|
||||||
|
val name = binding.etName.text.toString().trim()
|
||||||
|
val note = binding.etNote.text.toString().trim()
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
binding.tilName.error = "姓名不能为空"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.tilName.error = null
|
||||||
|
|
||||||
|
val token = prefs.getTokenSync() ?: return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.getApiService().updateContact(
|
||||||
|
"Bearer $token",
|
||||||
|
contactId,
|
||||||
|
UpdateContactRequest(name, note)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful && response.body()?.code == 200) {
|
||||||
|
Toast.makeText(this@ContactDetailActivity, "保存成功", Toast.LENGTH_SHORT).show()
|
||||||
|
binding.toolbar.title = name
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@ContactDetailActivity,
|
||||||
|
response.body()?.message ?: "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@ContactDetailActivity,
|
||||||
|
"网络错误: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadContactEmails() {
|
||||||
|
val token = prefs.getTokenSync() ?: return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.getApiService().getContactEmails(
|
||||||
|
"Bearer $token",
|
||||||
|
contactId
|
||||||
|
)
|
||||||
|
if (response.isSuccessful && response.body()?.code == 200) {
|
||||||
|
val emails = response.body()?.data?.emails ?: emptyList()
|
||||||
|
adapter.submitList(emails)
|
||||||
|
binding.tvEmpty.visibility = if (emails.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@ContactDetailActivity,
|
||||||
|
response.body()?.message ?: "获取邮件失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@ContactDetailActivity,
|
||||||
|
"网络错误: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(loading: Boolean) {
|
||||||
|
binding.progressBar.visibility = if (loading) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
loadContactEmails()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import com.example.emailclient.data.StarByInfoRequest
|
||||||
|
import com.example.emailclient.databinding.ActivityEmailDetailBinding
|
||||||
|
import com.example.emailclient.network.RetrofitClient
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件详情界面 - 显示通过POP3获取的邮件内容
|
||||||
|
*/
|
||||||
|
class EmailDetailActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityEmailDetailBinding
|
||||||
|
private var isStarred = false
|
||||||
|
private var senderAddress = ""
|
||||||
|
private var emailSubject = ""
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityEmailDetailBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
|
||||||
|
senderAddress = intent.getStringExtra("from") ?: ""
|
||||||
|
val to = intent.getStringExtra("to") ?: ""
|
||||||
|
emailSubject = intent.getStringExtra("subject") ?: "(无主题)"
|
||||||
|
val body = intent.getStringExtra("body") ?: ""
|
||||||
|
|
||||||
|
binding.tvFrom.text = senderAddress.ifEmpty { "未知发件人" }
|
||||||
|
binding.tvTo.text = to.ifEmpty { "未知收件人" }
|
||||||
|
binding.tvSubject.text = emailSubject
|
||||||
|
binding.tvBody.text = body
|
||||||
|
binding.tvDate.text = "通过POP3获取"
|
||||||
|
|
||||||
|
// 检查星标状态
|
||||||
|
checkStarredStatus()
|
||||||
|
|
||||||
|
// 星标按钮
|
||||||
|
binding.btnStar.setOnClickListener { toggleStar() }
|
||||||
|
|
||||||
|
// 回复按钮 - 通过SMTP发送回复
|
||||||
|
binding.fabReply.setOnClickListener {
|
||||||
|
val intent = Intent(this, ComposeActivity::class.java).apply {
|
||||||
|
putExtra("reply_to", senderAddress)
|
||||||
|
putExtra("subject", emailSubject)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStarButton() {
|
||||||
|
binding.btnStar.text = if (isStarred) "⭐ 已星标" else "☆ 星标"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkStarredStatus() {
|
||||||
|
val token = EmailApp.instance.preferencesManager.getTokenSync() ?: return
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.getApiService().checkStarred(
|
||||||
|
"Bearer $token",
|
||||||
|
StarByInfoRequest(senderAddress, emailSubject)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful && response.body()?.code == 200) {
|
||||||
|
val data = response.body()?.data
|
||||||
|
if (data != null && data.found) {
|
||||||
|
isStarred = data.isStarred
|
||||||
|
updateStarButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 忽略错误,保持默认状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleStar() {
|
||||||
|
val token = EmailApp.instance.preferencesManager.getTokenSync() ?: return
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.getApiService().toggleStarByInfo(
|
||||||
|
"Bearer $token",
|
||||||
|
StarByInfoRequest(senderAddress, emailSubject)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful && response.body()?.code == 200) {
|
||||||
|
val data = response.body()?.data
|
||||||
|
if (data != null) {
|
||||||
|
isStarred = data.isStarred
|
||||||
|
updateStarButton()
|
||||||
|
Toast.makeText(
|
||||||
|
this@EmailDetailActivity,
|
||||||
|
if (isStarred) "已添加星标" else "已取消星标",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else if (response.code() == 401 || response.body()?.code == 401) {
|
||||||
|
Toast.makeText(this@EmailDetailActivity, "登录已过期,请重新登录", Toast.LENGTH_SHORT).show()
|
||||||
|
EmailApp.instance.preferencesManager.clearUserInfo()
|
||||||
|
startActivity(Intent(this@EmailDetailActivity, LoginActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@EmailDetailActivity, "操作失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@EmailDetailActivity, "网络错误", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import com.example.emailclient.data.local.LocalEmail
|
||||||
|
import com.example.emailclient.databinding.ActivityLocalEmailDetailBinding
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class LocalEmailDetailActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityLocalEmailDetailBinding
|
||||||
|
private val repository by lazy { EmailApp.instance.emailRepository }
|
||||||
|
private var emailId: Long = 0
|
||||||
|
private var email: LocalEmail? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityLocalEmailDetailBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
|
||||||
|
emailId = intent.getLongExtra("email_id", 0)
|
||||||
|
if (emailId == 0L) {
|
||||||
|
Toast.makeText(this, "邮件不存在", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEmail()
|
||||||
|
setupButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadEmail() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
email = repository.getEmailById(emailId)
|
||||||
|
email?.let { displayEmail(it) } ?: run {
|
||||||
|
Toast.makeText(this@LocalEmailDetailActivity, "邮件不存在", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayEmail(email: LocalEmail) {
|
||||||
|
binding.tvSubject.text = email.subject.ifEmpty { "(无主题)" }
|
||||||
|
binding.tvFrom.text = "发件人: ${email.senderAddress}"
|
||||||
|
binding.tvTo.text = "收件人: ${email.recipientAddress}"
|
||||||
|
binding.tvDate.text = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
.format(Date(email.createdAt))
|
||||||
|
binding.tvBody.text = email.body
|
||||||
|
|
||||||
|
updateStarButton(email.isStarred)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStarButton(isStarred: Boolean) {
|
||||||
|
binding.btnStar.setImageResource(
|
||||||
|
if (isStarred) android.R.drawable.btn_star_big_on
|
||||||
|
else android.R.drawable.btn_star_big_off
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupButtons() {
|
||||||
|
binding.btnStar.setOnClickListener {
|
||||||
|
toggleStar()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnReply.setOnClickListener {
|
||||||
|
email?.let {
|
||||||
|
val intent = Intent(this, ComposeActivity::class.java).apply {
|
||||||
|
putExtra("reply_to", it.senderAddress)
|
||||||
|
putExtra("subject", it.subject)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnDelete.setOnClickListener {
|
||||||
|
showDeleteDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleStar() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repository.toggleStar(emailId)
|
||||||
|
email = repository.getEmailById(emailId)
|
||||||
|
email?.let {
|
||||||
|
updateStarButton(it.isStarred)
|
||||||
|
Toast.makeText(
|
||||||
|
this@LocalEmailDetailActivity,
|
||||||
|
if (it.isStarred) "已添加星标" else "已取消星标",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteDialog() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("删除邮件")
|
||||||
|
.setMessage("确定要删除这封邮件吗?")
|
||||||
|
.setPositiveButton("删除") { _, _ ->
|
||||||
|
deleteEmail()
|
||||||
|
}
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteEmail() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repository.deleteEmail(emailId)
|
||||||
|
Toast.makeText(this@LocalEmailDetailActivity, "已删除", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import com.example.emailclient.data.LoginRequest
|
||||||
|
import com.example.emailclient.databinding.ActivityLoginBinding
|
||||||
|
import com.example.emailclient.network.RetrofitClient
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityLoginBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
binding.btnLogin.setOnClickListener { login() }
|
||||||
|
binding.btnRegister.setOnClickListener {
|
||||||
|
startActivity(Intent(this, RegisterActivity::class.java))
|
||||||
|
}
|
||||||
|
binding.btnSettings.setOnClickListener {
|
||||||
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login() {
|
||||||
|
val username = binding.etUsername.text.toString().trim()
|
||||||
|
val password = binding.etPassword.text.toString()
|
||||||
|
|
||||||
|
if (username.isEmpty()) {
|
||||||
|
binding.tilUsername.error = "请输入用户名"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
binding.tilPassword.error = "请输入密码"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tilUsername.error = null
|
||||||
|
binding.tilPassword.error = null
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.getApiService().login(LoginRequest(username, password))
|
||||||
|
if (response.isSuccessful && response.body()?.code == 200) {
|
||||||
|
val data = response.body()?.data!!
|
||||||
|
EmailApp.instance.preferencesManager.saveUserInfo(
|
||||||
|
data.token,
|
||||||
|
data.user.username,
|
||||||
|
data.user.email,
|
||||||
|
data.user.isAdmin
|
||||||
|
)
|
||||||
|
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@LoginActivity,
|
||||||
|
response.body()?.message ?: "登录失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@LoginActivity,
|
||||||
|
"网络错误: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(loading: Boolean) {
|
||||||
|
binding.progressBar.visibility = if (loading) View.VISIBLE else View.GONE
|
||||||
|
binding.btnLogin.isEnabled = !loading
|
||||||
|
binding.btnRegister.isEnabled = !loading
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.emailclient.data.RegisterRequest
|
||||||
|
import com.example.emailclient.databinding.ActivityRegisterBinding
|
||||||
|
import com.example.emailclient.network.RetrofitClient
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class RegisterActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityRegisterBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityRegisterBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
binding.btnRegister.setOnClickListener { register() }
|
||||||
|
binding.btnBack.setOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun register() {
|
||||||
|
val username = binding.etUsername.text.toString().trim()
|
||||||
|
val email = binding.etEmail.text.toString().trim()
|
||||||
|
val password = binding.etPassword.text.toString()
|
||||||
|
val confirmPassword = binding.etConfirmPassword.text.toString()
|
||||||
|
|
||||||
|
if (username.isEmpty()) {
|
||||||
|
binding.tilUsername.error = "请输入用户名"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (email.isEmpty()) {
|
||||||
|
binding.tilEmail.error = "请输入邮箱"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
binding.tilPassword.error = "请输入密码"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
binding.tilPassword.error = "密码至少6位"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password != confirmPassword) {
|
||||||
|
binding.tilConfirmPassword.error = "两次密码不一致"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearErrors()
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.getApiService().register(
|
||||||
|
RegisterRequest(username, email, password)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful && response.body()?.code == 200) {
|
||||||
|
Toast.makeText(this@RegisterActivity, "注册成功,请登录", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@RegisterActivity,
|
||||||
|
response.body()?.message ?: "注册失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@RegisterActivity,
|
||||||
|
"网络错误: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearErrors() {
|
||||||
|
binding.tilUsername.error = null
|
||||||
|
binding.tilEmail.error = null
|
||||||
|
binding.tilPassword.error = null
|
||||||
|
binding.tilConfirmPassword.error = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(loading: Boolean) {
|
||||||
|
binding.progressBar.visibility = if (loading) View.VISIBLE else View.GONE
|
||||||
|
binding.btnRegister.isEnabled = !loading
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import com.example.emailclient.R
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@SuppressLint("CustomSplashScreen")
|
||||||
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_splash)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
delay(1500)
|
||||||
|
|
||||||
|
val token = EmailApp.instance.preferencesManager.token.first()
|
||||||
|
val intent = if (token.isNullOrEmpty()) {
|
||||||
|
Intent(this@SplashActivity, LoginActivity::class.java)
|
||||||
|
} else {
|
||||||
|
Intent(this@SplashActivity, MainActivity::class.java)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import com.example.emailclient.data.local.LocalEmail
|
||||||
|
import com.example.emailclient.databinding.ActivityStarredBinding
|
||||||
|
import com.example.emailclient.ui.adapter.LocalEmailAdapter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 星标邮件页面 - 使用本地存储
|
||||||
|
*/
|
||||||
|
class StarredActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityStarredBinding
|
||||||
|
private lateinit var adapter: LocalEmailAdapter
|
||||||
|
private val repository by lazy { EmailApp.instance.emailRepository }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityStarredBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
|
||||||
|
setupRecyclerView()
|
||||||
|
binding.swipeRefresh.setOnRefreshListener { loadStarredEmails() }
|
||||||
|
|
||||||
|
loadStarredEmails()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
adapter = LocalEmailAdapter(
|
||||||
|
onClick = { email -> openEmail(email) },
|
||||||
|
onLongClick = { email -> showDeleteDialog(email) },
|
||||||
|
onStarClick = { email -> toggleStar(email) }
|
||||||
|
)
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openEmail(email: LocalEmail) {
|
||||||
|
if (email.isDraft) {
|
||||||
|
val intent = Intent(this, ComposeActivity::class.java).apply {
|
||||||
|
putExtra("draft_id", email.id)
|
||||||
|
putExtra("recipient", email.recipientAddress)
|
||||||
|
putExtra("subject", email.subject)
|
||||||
|
putExtra("body", email.body)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
} else {
|
||||||
|
val intent = Intent(this, LocalEmailDetailActivity::class.java).apply {
|
||||||
|
putExtra("email_id", email.id)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteDialog(email: LocalEmail) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("删除邮件")
|
||||||
|
.setMessage("确定要删除这封邮件吗?")
|
||||||
|
.setPositiveButton("删除") { _, _ ->
|
||||||
|
deleteEmail(email)
|
||||||
|
}
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteEmail(email: LocalEmail) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repository.deleteEmail(email.id)
|
||||||
|
Toast.makeText(this@StarredActivity, "已删除", Toast.LENGTH_SHORT).show()
|
||||||
|
loadStarredEmails()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadStarredEmails() {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val emails = repository.getStarredEmails()
|
||||||
|
binding.swipeRefresh.isRefreshing = false
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
adapter.submitList(emails)
|
||||||
|
binding.tvEmpty.visibility = if (emails.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleStar(email: LocalEmail) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repository.toggleStar(email.id)
|
||||||
|
// 取消星标后从列表移除
|
||||||
|
loadStarredEmails()
|
||||||
|
Toast.makeText(this@StarredActivity, "已取消星标", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(loading: Boolean) {
|
||||||
|
binding.progressBar.visibility = if (loading) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
loadStarredEmails()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
package com.example.emailclient.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.example.emailclient.EmailApp
|
||||||
|
import com.example.emailclient.databinding.ActivityStarredEmailDetailBinding
|
||||||
|
import com.example.emailclient.network.RetrofitClient
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class StarredEmailDetailActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityStarredEmailDetailBinding
|
||||||
|
private var emailId: Int = 0
|
||||||
|
private var isStarred: Boolean = true
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityStarredEmailDetailBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbar)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { finish() }
|
||||||
|
|
||||||
|
emailId = intent.getIntExtra("email_id", 0)
|
||||||
|
val from = intent.getStringExtra("from") ?: ""
|
||||||
|
val to = intent.getStringExtra("to") ?: ""
|
||||||
|
val subject = intent.getStringExtra("subject") ?: "(无主题)"
|
||||||
|
val body = intent.getStringExtra("body") ?: ""
|
||||||
|
val date = intent.getStringExtra("date") ?: ""
|
||||||
|
isStarred = intent.getBooleanExtra("is_starred", true)
|
||||||
|
|
||||||
|
binding.tvFrom.text = from.ifEmpty { "未知发件人" }
|
||||||
|
binding.tvTo.text = to.ifEmpty { "未知收件人" }
|
||||||
|
binding.tvSubject.text = subject
|
||||||
|
binding.tvBody.text = body
|
||||||
|
binding.tvDate.text = date.take(19).replace("T", " ")
|
||||||
|
|
||||||
|
updateStarButton()
|
||||||
|
|
||||||
|
binding.btnStar.setOnClickListener { toggleStar() }
|
||||||
|
|
||||||
|
binding.fabReply.setOnClickListener {
|
||||||
|
val intent = Intent(this, ComposeActivity::class.java).apply {
|
||||||
|
putExtra("reply_to", from)
|
||||||
|
putExtra("subject", subject)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStarButton() {
|
||||||
|
binding.btnStar.text = if (isStarred) "⭐ 已星标" else "☆ 添加星标"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleStar() {
|
||||||
|
val token = EmailApp.instance.preferencesManager.getTokenSync() ?: return
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitClient.getApiService().toggleStar("Bearer $token", emailId)
|
||||||
|
if (response.isSuccessful && response.body()?.code == 200) {
|
||||||
|
val data = response.body()?.data
|
||||||
|
if (data != null) {
|
||||||
|
isStarred = data.isStarred
|
||||||
|
updateStarButton()
|
||||||
|
Toast.makeText(
|
||||||
|
this@StarredEmailDetailActivity,
|
||||||
|
if (isStarred) "已添加星标" else "已取消星标",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else if (response.code() == 401 || response.body()?.code == 401) {
|
||||||
|
Toast.makeText(this@StarredEmailDetailActivity, "登录已过期,请重新登录", Toast.LENGTH_SHORT).show()
|
||||||
|
EmailApp.instance.preferencesManager.clearUserInfo()
|
||||||
|
startActivity(Intent(this@StarredEmailDetailActivity, LoginActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@StarredEmailDetailActivity, "操作失败", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.R
|
||||||
|
import com.example.emailclient.data.AdminUser
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
|
class AdminUserAdapter(
|
||||||
|
private val users: MutableList<AdminUser>,
|
||||||
|
private val onToggle: (AdminUser) -> Unit,
|
||||||
|
private val onDelete: (AdminUser) -> Unit
|
||||||
|
) : RecyclerView.Adapter<AdminUserAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val tvUsername: TextView = view.findViewById(R.id.tvUsername)
|
||||||
|
val tvEmail: TextView = view.findViewById(R.id.tvEmail)
|
||||||
|
val tvRole: TextView = view.findViewById(R.id.tvRole)
|
||||||
|
val tvStatus: TextView = view.findViewById(R.id.tvStatus)
|
||||||
|
val btnToggle: MaterialButton = view.findViewById(R.id.btnToggle)
|
||||||
|
val btnDelete: MaterialButton = view.findViewById(R.id.btnDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_admin_user, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val user = users[position]
|
||||||
|
|
||||||
|
holder.tvUsername.text = user.username
|
||||||
|
holder.tvEmail.text = user.email
|
||||||
|
|
||||||
|
holder.tvRole.text = if (user.isAdmin) "管理员" else "普通用户"
|
||||||
|
holder.tvRole.background.setTint(if (user.isAdmin) Color.parseColor("#FF9800") else Color.parseColor("#4CAF50"))
|
||||||
|
|
||||||
|
holder.tvStatus.text = if (user.isActive) "状态: 启用" else "状态: 禁用"
|
||||||
|
holder.tvStatus.setTextColor(if (user.isActive) Color.parseColor("#4CAF50") else Color.parseColor("#F44336"))
|
||||||
|
|
||||||
|
holder.btnToggle.text = if (user.isActive) "禁用" else "启用"
|
||||||
|
holder.btnToggle.setOnClickListener { onToggle(user) }
|
||||||
|
holder.btnDelete.setOnClickListener { onDelete(user) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = users.size
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.data.Contact
|
||||||
|
import com.example.emailclient.databinding.ItemContactBinding
|
||||||
|
|
||||||
|
class ContactAdapter(
|
||||||
|
private val onClick: (Contact) -> Unit,
|
||||||
|
private val onDelete: (Contact) -> Unit
|
||||||
|
) : ListAdapter<Contact, ContactAdapter.ViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemContactBinding,
|
||||||
|
private val onClick: (Contact) -> Unit,
|
||||||
|
private val onDelete: (Contact) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(contact: Contact) {
|
||||||
|
binding.tvName.text = contact.name
|
||||||
|
binding.tvEmail.text = contact.email
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { onClick(contact) }
|
||||||
|
binding.btnDelete.setOnClickListener { onDelete(contact) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemContactBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return ViewHolder(binding, onClick, onDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<Contact>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Contact, newItem: Contact) =
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
override fun areContentsTheSame(oldItem: Contact, newItem: Contact) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.R
|
||||||
|
import com.example.emailclient.data.ContactEmail
|
||||||
|
import com.example.emailclient.databinding.ItemContactEmailBinding
|
||||||
|
|
||||||
|
class ContactEmailAdapter(
|
||||||
|
private val onClick: (ContactEmail) -> Unit
|
||||||
|
) : ListAdapter<ContactEmail, ContactEmailAdapter.ViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemContactEmailBinding,
|
||||||
|
private val onClick: (ContactEmail) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(email: ContactEmail) {
|
||||||
|
// 显示方向标签
|
||||||
|
if (email.isSent) {
|
||||||
|
binding.tvDirection.text = "发出"
|
||||||
|
binding.tvDirection.setBackgroundColor(Color.parseColor("#4CAF50"))
|
||||||
|
} else {
|
||||||
|
binding.tvDirection.text = "收到"
|
||||||
|
binding.tvDirection.setBackgroundColor(Color.parseColor("#2196F3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tvSubject.text = email.subject
|
||||||
|
binding.tvPreview.text = email.body.take(100).replace("\n", " ")
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
val date = email.createdAt.substringBefore("T")
|
||||||
|
binding.tvDate.text = date
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { onClick(email) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemContactEmailBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return ViewHolder(binding, onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<ContactEmail>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ContactEmail, newItem: ContactEmail) =
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
override fun areContentsTheSame(oldItem: ContactEmail, newItem: ContactEmail) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.databinding.ItemEmailBinding
|
||||||
|
import com.example.emailclient.network.Pop3Email
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class EmailAdapter(
|
||||||
|
private val onClick: (Pop3Email) -> Unit,
|
||||||
|
private val onDelete: (Pop3Email) -> Unit
|
||||||
|
) : ListAdapter<Pop3Email, EmailAdapter.ViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemEmailBinding,
|
||||||
|
private val onClick: (Pop3Email) -> Unit,
|
||||||
|
private val onDelete: (Pop3Email) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(email: Pop3Email) {
|
||||||
|
binding.tvSender.text = email.from.ifEmpty { "未知发件人" }
|
||||||
|
binding.tvSubject.text = email.subject
|
||||||
|
binding.tvPreview.text = email.body.take(100).replace("\n", " ")
|
||||||
|
binding.tvDate.text = SimpleDateFormat("MM/dd", Locale.getDefault()).format(Date())
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { onClick(email) }
|
||||||
|
binding.root.setOnLongClickListener {
|
||||||
|
onDelete(email)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemEmailBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return ViewHolder(binding, onClick, onDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<Pop3Email>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Pop3Email, newItem: Pop3Email) =
|
||||||
|
oldItem.uid == newItem.uid
|
||||||
|
override fun areContentsTheSame(oldItem: Pop3Email, newItem: Pop3Email) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.R
|
||||||
|
import com.example.emailclient.data.EmailFilter
|
||||||
|
|
||||||
|
class FilterAdapter(
|
||||||
|
private val filters: MutableList<EmailFilter>,
|
||||||
|
private val onDelete: (EmailFilter) -> Unit
|
||||||
|
) : RecyclerView.Adapter<FilterAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val tvType: TextView = view.findViewById(R.id.tvType)
|
||||||
|
val tvValue: TextView = view.findViewById(R.id.tvValue)
|
||||||
|
val tvAction: TextView = view.findViewById(R.id.tvAction)
|
||||||
|
val btnDelete: ImageButton = view.findViewById(R.id.btnDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_filter, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val filter = filters[position]
|
||||||
|
|
||||||
|
holder.tvType.text = if (filter.filterType == "email") "邮箱" else "IP"
|
||||||
|
holder.tvType.background.setTint(if (filter.filterType == "email") Color.parseColor("#4CAF50") else Color.parseColor("#FF9800"))
|
||||||
|
|
||||||
|
holder.tvValue.text = filter.value
|
||||||
|
|
||||||
|
holder.tvAction.text = if (filter.action == "block") "阻止" else "允许"
|
||||||
|
holder.tvAction.background.setTint(if (filter.action == "block") Color.parseColor("#F44336") else Color.parseColor("#4CAF50"))
|
||||||
|
|
||||||
|
holder.btnDelete.setOnClickListener { onDelete(filter) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = filters.size
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.R
|
||||||
|
import com.example.emailclient.data.local.LocalEmail
|
||||||
|
import com.example.emailclient.databinding.ItemLocalEmailBinding
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class LocalEmailAdapter(
|
||||||
|
private val onClick: (LocalEmail) -> Unit,
|
||||||
|
private val onLongClick: (LocalEmail) -> Unit,
|
||||||
|
private val onStarClick: ((LocalEmail) -> Unit)? = null
|
||||||
|
) : ListAdapter<LocalEmail, LocalEmailAdapter.ViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemLocalEmailBinding,
|
||||||
|
private val onClick: (LocalEmail) -> Unit,
|
||||||
|
private val onLongClick: (LocalEmail) -> Unit,
|
||||||
|
private val onStarClick: ((LocalEmail) -> Unit)?
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("MM/dd HH:mm", Locale.getDefault())
|
||||||
|
|
||||||
|
fun bind(email: LocalEmail) {
|
||||||
|
// 根据是发送还是接收显示不同的地址
|
||||||
|
binding.tvSender.text = if (email.isSent || email.isDraft) {
|
||||||
|
"收件人: ${email.recipientAddress.ifEmpty { "未填写" }}"
|
||||||
|
} else {
|
||||||
|
email.senderAddress.ifEmpty { "未知发件人" }
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tvSubject.text = email.subject.ifEmpty { "(无主题)" }
|
||||||
|
binding.tvPreview.text = email.body.take(100).replace("\n", " ")
|
||||||
|
binding.tvDate.text = dateFormat.format(Date(email.createdAt))
|
||||||
|
|
||||||
|
// 未读标记
|
||||||
|
if (!email.isRead && !email.isSent && !email.isDraft) {
|
||||||
|
binding.tvSubject.setTextColor(binding.root.context.getColor(R.color.primary))
|
||||||
|
} else {
|
||||||
|
binding.tvSubject.setTextColor(binding.root.context.getColor(R.color.black))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 星标按钮
|
||||||
|
if (onStarClick != null) {
|
||||||
|
binding.ivStar.visibility = View.VISIBLE
|
||||||
|
binding.ivStar.setImageResource(
|
||||||
|
if (email.isStarred) android.R.drawable.btn_star_big_on
|
||||||
|
else android.R.drawable.btn_star_big_off
|
||||||
|
)
|
||||||
|
binding.ivStar.setOnClickListener { onStarClick.invoke(email) }
|
||||||
|
} else {
|
||||||
|
binding.ivStar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// 草稿标记
|
||||||
|
if (email.isDraft) {
|
||||||
|
binding.tvDraftTag.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.tvDraftTag.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { onClick(email) }
|
||||||
|
binding.root.setOnLongClickListener {
|
||||||
|
onLongClick(email)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemLocalEmailBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return ViewHolder(binding, onClick, onLongClick, onStarClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<LocalEmail>() {
|
||||||
|
override fun areItemsTheSame(oldItem: LocalEmail, newItem: LocalEmail) =
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
override fun areContentsTheSame(oldItem: LocalEmail, newItem: LocalEmail) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.R
|
||||||
|
import com.example.emailclient.data.LogEntry
|
||||||
|
|
||||||
|
class LogAdapter(
|
||||||
|
private val logs: MutableList<LogEntry>
|
||||||
|
) : RecyclerView.Adapter<LogAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val tvLevel: TextView = view.findViewById(R.id.tvLevel)
|
||||||
|
val tvSource: TextView = view.findViewById(R.id.tvSource)
|
||||||
|
val tvTime: TextView = view.findViewById(R.id.tvTime)
|
||||||
|
val tvMessage: TextView = view.findViewById(R.id.tvMessage)
|
||||||
|
val tvIp: TextView = view.findViewById(R.id.tvIp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_log, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val log = logs[position]
|
||||||
|
|
||||||
|
holder.tvLevel.text = log.level
|
||||||
|
holder.tvLevel.background.setTint(when (log.level) {
|
||||||
|
"ERROR" -> Color.parseColor("#F44336")
|
||||||
|
"WARNING" -> Color.parseColor("#FF9800")
|
||||||
|
else -> Color.parseColor("#4CAF50")
|
||||||
|
})
|
||||||
|
|
||||||
|
holder.tvSource.text = log.source ?: "-"
|
||||||
|
holder.tvTime.text = log.createdAt.replace("T", " ").take(19)
|
||||||
|
holder.tvMessage.text = log.message
|
||||||
|
holder.tvIp.text = if (log.ipAddress.isNullOrEmpty()) "" else "IP: ${log.ipAddress}"
|
||||||
|
holder.tvIp.visibility = if (log.ipAddress.isNullOrEmpty()) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = logs.size
|
||||||
|
|
||||||
|
fun addLogs(newLogs: List<LogEntry>) {
|
||||||
|
val startPos = logs.size
|
||||||
|
logs.addAll(newLogs)
|
||||||
|
notifyItemRangeInserted(startPos, newLogs.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.example.emailclient.ui.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.emailclient.data.StarredEmail
|
||||||
|
import com.example.emailclient.databinding.ItemStarredEmailBinding
|
||||||
|
|
||||||
|
class StarredEmailAdapter(
|
||||||
|
private val onClick: (StarredEmail) -> Unit,
|
||||||
|
private val onStarClick: (StarredEmail) -> Unit
|
||||||
|
) : ListAdapter<StarredEmail, StarredEmailAdapter.ViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemStarredEmailBinding,
|
||||||
|
private val onClick: (StarredEmail) -> Unit,
|
||||||
|
private val onStarClick: (StarredEmail) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(email: StarredEmail) {
|
||||||
|
binding.tvSender.text = email.senderAddress.ifEmpty { "未知发件人" }
|
||||||
|
binding.tvSubject.text = email.subject
|
||||||
|
binding.tvPreview.text = email.body.take(100).replace("\n", " ")
|
||||||
|
binding.tvDate.text = email.createdAt.take(10)
|
||||||
|
binding.btnStar.text = if (email.isStarred) "⭐" else "☆"
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { onClick(email) }
|
||||||
|
binding.btnStar.setOnClickListener { onStarClick(email) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemStarredEmailBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context), parent, false
|
||||||
|
)
|
||||||
|
return ViewHolder(binding, onClick, onStarClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<StarredEmail>() {
|
||||||
|
override fun areItemsTheSame(oldItem: StarredEmail, newItem: StarredEmail) =
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
override fun areContentsTheSame(oldItem: StarredEmail, newItem: StarredEmail) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/purple_500" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="@color/light_gray" />
|
||||||
|
</shape>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="@color/white">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<!-- 邮件图标 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M54,30L24,50L24,78L84,78L84,50L54,30ZM54,36L78,52L78,72L30,72L30,52L54,36ZM54,42L36,54L72,54L54,42Z"/>
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,285 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
app:titleTextColor="@android:color/white" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tabLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:tabMode="scrollable"
|
||||||
|
app:tabGravity="start" />
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<!-- 仪表盘 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/dashboardLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="系统概览"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvUserCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="0"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/purple_500" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/user_count"
|
||||||
|
android:textColor="@android:color/darker_gray" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmailCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="0"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/teal_700" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/email_count"
|
||||||
|
android:textColor="@android:color/darker_gray" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSmtpPort"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="25"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#FF5722" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/smtp_port"
|
||||||
|
android:textColor="@android:color/darker_gray" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPop3Port"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="110"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#9C27B0" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/pop3_port"
|
||||||
|
android:textColor="@android:color/darker_gray" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:padding="8dp" />
|
||||||
|
|
||||||
|
<!-- 群发邮件 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/broadcastLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="向所有用户发送邮件"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/subject">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etBroadcastSubject"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/content">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etBroadcastBody"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:gravity="top" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnSendBroadcast"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/send_broadcast" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnAdd"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:text="添加"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:icon="@android:drawable/ic_input_add" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnClearLogs"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|start"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:text="@string/clear_logs"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:backgroundTint="#F44336" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnLoadMore"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:text="加载更多"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="@string/compose"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilRecipient"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/recipient">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etRecipient"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilSubject"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/subject">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etSubject"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilBody"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="@string/content">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etBody"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="top"
|
||||||
|
android:inputType="textMultiLine" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSaveDraft"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/save_draft" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSend"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/send" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="联系人详情"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<!-- 联系人信息卡片 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilName"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="姓名">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilNote"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:hint="备注">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etNote"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:minLines="2" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSave"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="保存修改" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- 邮件往来 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="邮件往来"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmpty"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="暂无邮件往来"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fabCompose"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@android:drawable/ic_dialog_email"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="@string/contacts"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmpty"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="暂无联系人"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fabAdd"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@android:drawable/ic_input_add"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="邮件详情"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSubject"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/black" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnStar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="☆ 星标"
|
||||||
|
android:textSize="14sp"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="发件人: "
|
||||||
|
android:textColor="@color/gray" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvFrom"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="收件人: "
|
||||||
|
android:textColor="@color/gray" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvBody"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:lineSpacingMultiplier="1.3" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fabReply"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@android:drawable/ic_menu_send"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="邮件详情"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<!-- 主题 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSubject"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<!-- 发件人/收件人信息 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="@drawable/circle_bg"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@android:drawable/ic_menu_myplaces"
|
||||||
|
android:contentDescription="头像" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvFrom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnStar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/btn_star_big_off"
|
||||||
|
android:contentDescription="星标" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<!-- 邮件正文 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvBody"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lineSpacingExtra="4dp"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@color/white"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnReply"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="回复" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnDelete"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="删除"
|
||||||
|
android:textColor="@color/accent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/login"
|
||||||
|
android:textSize="32sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:layout_marginTop="60dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilUsername"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:hint="@string/username"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tvTitle">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etUsername"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilPassword"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/password"
|
||||||
|
app:endIconMode="password_toggle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilUsername">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etPassword"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnLogin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:text="@string/login"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilPassword" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnRegister"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/register"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/btnLogin" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSettings"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="服务器设置"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/btnRegister" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:title="@string/inbox"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tabLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:tabIndicatorColor="@color/white"
|
||||||
|
app:tabSelectedTextColor="@color/white"
|
||||||
|
app:tabTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmpty"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/no_emails"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fabCompose"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@android:drawable/ic_input_add"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/register"
|
||||||
|
android:textSize="32sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:layout_marginTop="60dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilUsername"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:hint="@string/username"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tvTitle">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etUsername"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilEmail"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/email"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilUsername">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etEmail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilPassword"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/password"
|
||||||
|
app:endIconMode="password_toggle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilEmail">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etPassword"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilConfirmPassword"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/confirm_password"
|
||||||
|
app:endIconMode="password_toggle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilPassword">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etConfirmPassword"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnRegister"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:text="@string/register"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilConfirmPassword" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnBack"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="返回登录"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/btnRegister" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="@string/settings"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="服务器配置"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/primary" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilServerHost"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/server_host">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etServerHost"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilSmtpPort"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/smtp_port">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etSmtpPort"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilPop3Port"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/pop3_port">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etPop3Port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilApiPort"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/api_port">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etApiPort"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSave"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/save" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="24dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnLogout"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/logout"
|
||||||
|
android:textColor="@color/accent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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/primary">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivLogo"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:src="@android:drawable/ic_dialog_email"
|
||||||
|
android:tint="@color/white"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/tvAppName"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAppName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/ivLogo" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="⭐ 星标邮件"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefresh"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmpty"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="暂无星标邮件"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="邮件详情"
|
||||||
|
app:titleTextColor="@color/white" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSubject"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/black" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnStar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="⭐ 已星标"
|
||||||
|
android:textSize="14sp"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="发件人: "
|
||||||
|
android:textColor="@color/gray" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvFrom"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="收件人: "
|
||||||
|
android:textColor="@color/gray" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvBody"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:lineSpacingMultiplier="1.3" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fabReply"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@android:drawable/ic_menu_send"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/name">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/email">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etEmail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/note">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etNote"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/filter_type"
|
||||||
|
android:textColor="@android:color/darker_gray" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerType"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/filter_value">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/filter_action"
|
||||||
|
android:textColor="@android:color/darker_gray" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerAction"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/username">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etUsername"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/email">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etEmail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/password">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etPassword"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/cbIsAdmin"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/is_admin" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView 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="wrap_content"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvUsername"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvRole"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/badge_background"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnToggle"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnDelete"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:text="@string/delete"
|
||||||
|
android:textColor="#F44336"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
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="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:layout_marginVertical="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@android:drawable/ic_menu_myplaces"
|
||||||
|
android:tint="@color/primary" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmail"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnDelete"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/ic_menu_delete"
|
||||||
|
android:tint="@color/gray" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
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="wrap_content"
|
||||||
|
android:layout_marginVertical="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDirection"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/primary"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSubject"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
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="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:layout_marginVertical="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSender"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSubject"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView 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="wrap_content"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvType"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/badge_background"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAction"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="@drawable/badge_background"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnDelete"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@android:drawable/ic_menu_delete"
|
||||||
|
android:contentDescription="@string/delete" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
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="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:layout_marginVertical="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDraftTag"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/orange"
|
||||||
|
android:paddingHorizontal="6dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:text="草稿"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSender"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSubject"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivStar"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:contentDescription="星标"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:src="@android:drawable/btn_star_big_off"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView 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="wrap_content"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLevel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/badge_background"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSource"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTime"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvMessage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvIp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
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="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:layout_marginVertical="4dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnStar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:text="⭐"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:minHeight="0dp"
|
||||||
|
android:padding="0dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginStart="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSender"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSubject"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_starred"
|
||||||
|
android:icon="@android:drawable/btn_star_big_on"
|
||||||
|
android:title="@string/starred"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_contacts"
|
||||||
|
android:icon="@android:drawable/ic_menu_myplaces"
|
||||||
|
android:title="@string/contacts"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_admin"
|
||||||
|
android:icon="@android:drawable/ic_menu_manage"
|
||||||
|
android:title="@string/admin"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:icon="@android:drawable/ic_menu_preferences"
|
||||||
|
android:title="@string/settings"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_test_data"
|
||||||
|
android:title="添加测试数据"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/primary"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/primary"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="72"
|
||||||
|
android:viewportHeight="72">
|
||||||
|
<path
|
||||||
|
android:fillColor="#1976D2"
|
||||||
|
android:pathData="M0,0h72v72h-72z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M36,18L12,36v24h48V36L36,18zM36,24l18,13.5v16.5H18V37.5L36,24z"/>
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="72"
|
||||||
|
android:viewportHeight="72">
|
||||||
|
<path
|
||||||
|
android:fillColor="#1976D2"
|
||||||
|
android:pathData="M36,36m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M36,21L15,36v21h42V36L36,21zM36,27l15,10.5v13.5H21V37.5L36,27z"/>
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#1976D2"
|
||||||
|
android:pathData="M0,0h48v48h-48z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M24,12L8,24v16h32V24L24,12zM24,16l12,9v11H12V25l12,-9z"/>
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#1976D2"
|
||||||
|
android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M24,14L10,24v14h28V24L24,14zM24,18l10,7v9H14V25l10,-7z"/>
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue