Compare commits
17 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
49087887ba | 3 weeks ago |
|
|
314a88f7eb | 4 weeks ago |
|
|
56695e1657 | 4 weeks ago |
|
|
6898b82a65 | 4 weeks ago |
|
|
6e24476885 | 4 weeks ago |
|
|
536b49b2a4 | 4 weeks ago |
|
|
c1969d838c | 4 weeks ago |
|
|
60bb404320 | 4 weeks ago |
|
|
2fd73a74a4 | 4 weeks ago |
|
|
0f4b6f09ac | 1 month ago |
|
|
9c22f14a96 | 1 month ago |
|
|
cbea749514 | 1 month ago |
|
|
c98d3193f9 | 1 month ago |
|
|
91fe249ba5 | 1 month ago |
|
|
e118c83a80 | 1 month ago |
|
|
acc94815e1 | 1 month ago |
|
|
c69d6a66c3 | 1 month 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