Skip to content

Commit 06d65a1

Browse files
committed
feat: redesign home with Miku hero banner
- Replace the plain header + inline add-form with a Miku hero banner (particle animation + scrim + title) matching the menu/About styling; overlay the menu button on the banner. - Move add-subscription/import into an AddProfileBottomSheet opened by a '+' action beside the Profiles header; empty state now shows as a card. - Gate the home particle animation on the AppSettings toggle. - Translate action_add_profile for zh-rCN/zh-rTW/in/ru.
1 parent 55c73d4 commit 06d65a1

10 files changed

Lines changed: 238 additions & 99 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package top.uwu.mikubox.ui
2+
3+
import android.content.Context
4+
import android.os.Bundle
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
9+
import com.google.android.material.textfield.TextInputEditText
10+
import top.uwu.mikubox.R
11+
12+
/** Bottom sheet for adding a subscription or importing a config from clipboard. */
13+
class AddProfileBottomSheet : BottomSheetDialogFragment() {
14+
15+
interface Listener {
16+
fun onAddSubscription(url: String, name: String)
17+
fun onImportClipboard(name: String)
18+
}
19+
20+
private var listener: Listener? = null
21+
22+
override fun onAttach(context: Context) {
23+
super.onAttach(context)
24+
listener = context as? Listener
25+
}
26+
27+
override fun onCreateView(
28+
inflater: LayoutInflater,
29+
container: ViewGroup?,
30+
savedInstanceState: Bundle?,
31+
): View = inflater.inflate(R.layout.layout_add_sheet, container, false)
32+
33+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
34+
super.onViewCreated(view, savedInstanceState)
35+
val url = view.findViewById<TextInputEditText>(R.id.et_sub_url)
36+
val name = view.findViewById<TextInputEditText>(R.id.et_name)
37+
38+
view.findViewById<View>(R.id.btn_add_sub).setOnClickListener {
39+
listener?.onAddSubscription(
40+
url.text?.toString()?.trim().orEmpty(),
41+
name.text?.toString()?.trim().orEmpty(),
42+
)
43+
dismiss()
44+
}
45+
view.findViewById<View>(R.id.btn_import_clipboard).setOnClickListener {
46+
listener?.onImportClipboard(name.text?.toString()?.trim().orEmpty())
47+
dismiss()
48+
}
49+
}
50+
51+
override fun onDetach() {
52+
super.onDetach()
53+
listener = null
54+
}
55+
56+
companion object {
57+
const val TAG = "AddProfileBottomSheet"
58+
}
59+
}

app/src/main/java/top/uwu/mikubox/ui/MainActivity.kt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers
1818
import kotlinx.coroutines.launch
1919
import kotlinx.coroutines.withContext
2020
import top.uwu.mikubox.R
21+
import top.uwu.mikubox.core.AppSettings
2122
import top.uwu.mikubox.core.MihomoCore
2223
import top.uwu.mikubox.databinding.ActivityMainBinding
2324
import top.uwu.mikubox.profile.MihomoProfileImporter
@@ -30,7 +31,7 @@ import top.uwu.mikubox.service.VpnController
3031
* the VPN. A faithful MikuRay-style redesign comes later; this restores the
3132
* core MikuBox workflow (import a subscription/config, then connect).
3233
*/
33-
class MainActivity : AppCompatActivity() {
34+
class MainActivity : AppCompatActivity(), AddProfileBottomSheet.Listener {
3435

3536
private lateinit var binding: ActivityMainBinding
3637
private lateinit var adapter: ProfileAdapter
@@ -63,8 +64,9 @@ class MainActivity : AppCompatActivity() {
6364
binding.rvProfiles.layoutManager = LinearLayoutManager(this)
6465
binding.rvProfiles.adapter = adapter
6566

66-
binding.btnAddSub.setOnClickListener { addSubscription() }
67-
binding.btnImportClipboard.setOnClickListener { importClipboard() }
67+
binding.btnAdd.setOnClickListener {
68+
AddProfileBottomSheet().show(supportFragmentManager, AddProfileBottomSheet.TAG)
69+
}
6870
binding.fab.setOnClickListener { toggleConnection() }
6971
binding.cardBottomStatus.setOnClickListener { toggleConnection() }
7072
binding.btnMenu.setOnClickListener {
@@ -89,7 +91,9 @@ class MainActivity : AppCompatActivity() {
8991
val profiles = MihomoProfileStore.profiles(this)
9092
val selected = MihomoProfileStore.selected(this)
9193
adapter.submit(profiles, selected?.id)
92-
binding.tvEmpty.visibility = if (profiles.isEmpty()) View.VISIBLE else View.GONE
94+
binding.emptyCard.visibility = if (profiles.isEmpty()) View.VISIBLE else View.GONE
95+
binding.particlesView.visibility =
96+
if (AppSettings.particlesEnabled(this)) View.VISIBLE else View.GONE
9397

9498
val running = VpnController.isRunning
9599
binding.fab.setImageResource(if (running) R.drawable.ic_service_busy else R.drawable.ic_service_idle)
@@ -125,9 +129,7 @@ class MainActivity : AppCompatActivity() {
125129
return String.format(java.util.Locale.US, "%.1f %s", value, units[unit])
126130
}
127131

128-
private fun addSubscription() {
129-
val url = binding.etSubUrl.text?.toString()?.trim().orEmpty()
130-
val name = binding.etName.text?.toString()?.trim().orEmpty()
132+
override fun onAddSubscription(url: String, name: String) {
131133
if (url.isEmpty()) {
132134
toast(getString(R.string.error_subscription_url_blank))
133135
return
@@ -138,24 +140,20 @@ class MainActivity : AppCompatActivity() {
138140
toast(getString(R.string.toast_import_failed, e.message ?: ""))
139141
return
140142
}
141-
binding.etSubUrl.text = null
142-
binding.etName.text = null
143143
toast(getString(R.string.toast_subscription_added))
144144
refresh()
145145
pull(profile)
146146
}
147147

148-
private fun importClipboard() {
148+
override fun onImportClipboard(name: String) {
149149
val clip = (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
150150
.primaryClip?.getItemAt(0)?.coerceToText(this)?.toString()?.trim().orEmpty()
151151
if (clip.isEmpty()) {
152152
toast(getString(R.string.toast_clipboard_empty))
153153
return
154154
}
155-
val name = binding.etName.text?.toString()?.trim().orEmpty()
156155
try {
157156
MihomoProfileImporter.importConfig(this, name, clip)
158-
binding.etName.text = null
159157
toast(getString(R.string.toast_config_imported))
160158
refresh()
161159
} catch (e: Exception) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<vector
3+
android:tint="?colorControlNormal"
4+
android:height="24.0dip"
5+
android:width="24.0dip"
6+
android:viewportWidth="960.0"
7+
android:viewportHeight="960.0"
8+
xmlns:android="http://schemas.android.com/apk/res/android">
9+
<path
10+
android:fillColor="@android:color/white"
11+
android:pathData="M440,680L520,680L520,520L680,520L680,440L520,440L520,280L440,280L440,440L280,440L280,520L440,520L440,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z" />
12+
</vector>

app/src/main/res/layout/activity_main.xml

Lines changed: 89 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -20,129 +20,131 @@
2020
android:layout_width="match_parent"
2121
android:layout_height="wrap_content"
2222
android:orientation="vertical"
23-
android:paddingStart="16dp"
24-
android:paddingTop="24dp"
25-
android:paddingEnd="16dp">
23+
android:paddingHorizontal="16dp"
24+
android:paddingTop="16dp">
2625

27-
<LinearLayout
26+
<!-- Miku hero banner with particle animation -->
27+
<com.google.android.material.card.MaterialCardView
2828
android:layout_width="match_parent"
29-
android:layout_height="wrap_content"
30-
android:layout_marginBottom="16dp"
31-
android:gravity="center_vertical"
32-
android:orientation="horizontal">
29+
android:layout_height="176dp"
30+
app:cardCornerRadius="28dp"
31+
app:cardElevation="0dp"
32+
app:strokeWidth="0dp">
33+
34+
<ImageView
35+
android:layout_width="match_parent"
36+
android:layout_height="match_parent"
37+
android:scaleType="centerCrop"
38+
android:src="@drawable/uwu_banner_sheet"
39+
tools:ignore="ContentDescription" />
40+
41+
<View
42+
android:layout_width="match_parent"
43+
android:layout_height="match_parent"
44+
android:background="@drawable/bg_about_scrim" />
45+
46+
<com.neko.particlesdrawable.ParticlesView
47+
android:id="@+id/particles_view"
48+
android:layout_width="match_parent"
49+
android:layout_height="match_parent"
50+
android:background="#00000000"
51+
app:dotColor="?attr/colorPrimary"
52+
app:frameDelayMillis="10"
53+
app:lineColor="?attr/colorPrimary"
54+
app:lineDistance="86dp"
55+
app:lineThickness="1dp"
56+
app:maxDotRadius="4dp"
57+
app:minDotRadius="1dp"
58+
app:numDots="55"
59+
app:stepMultiplier="1.0" />
60+
61+
<com.google.android.material.button.MaterialButton
62+
android:id="@+id/btn_menu"
63+
style="@style/Widget.Material3.Button.IconButton.Filled"
64+
android:layout_width="wrap_content"
65+
android:layout_height="wrap_content"
66+
android:layout_gravity="top|end"
67+
android:layout_margin="10dp"
68+
android:contentDescription="@string/app_settings"
69+
app:backgroundTint="#40000000"
70+
app:icon="@drawable/ic_navigation_menu"
71+
app:iconTint="@android:color/white" />
3372

3473
<LinearLayout
35-
android:layout_width="0dp"
74+
android:layout_width="wrap_content"
3675
android:layout_height="wrap_content"
37-
android:layout_weight="1"
38-
android:orientation="vertical">
76+
android:layout_gravity="bottom|start"
77+
android:orientation="vertical"
78+
android:padding="20dp">
3979

4080
<TextView
4181
android:layout_width="wrap_content"
4282
android:layout_height="wrap_content"
4383
android:text="@string/app_name"
44-
android:textColor="?attr/colorPrimary"
45-
android:textSize="30sp"
84+
android:textColor="@android:color/white"
85+
android:textSize="32sp"
4686
android:textStyle="bold" />
4787

4888
<TextView
4989
android:layout_width="wrap_content"
5090
android:layout_height="wrap_content"
5191
android:text="@string/scaffold_subtitle"
52-
android:textColor="?attr/colorOnSurfaceVariant"
92+
android:textColor="@android:color/white"
5393
android:textSize="14sp" />
5494
</LinearLayout>
95+
</com.google.android.material.card.MaterialCardView>
96+
97+
<!-- Profiles header + add action -->
98+
<LinearLayout
99+
android:layout_width="match_parent"
100+
android:layout_height="wrap_content"
101+
android:layout_marginTop="20dp"
102+
android:layout_marginBottom="8dp"
103+
android:gravity="center_vertical"
104+
android:orientation="horizontal">
105+
106+
<TextView
107+
android:layout_width="0dp"
108+
android:layout_height="wrap_content"
109+
android:layout_weight="1"
110+
android:layout_marginStart="4dp"
111+
android:text="@string/profiles_header"
112+
android:textColor="?attr/colorOnSurface"
113+
android:textSize="18sp"
114+
android:textStyle="bold" />
55115

56116
<com.google.android.material.button.MaterialButton
57-
android:id="@+id/btn_menu"
117+
android:id="@+id/btn_add"
58118
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
59119
android:layout_width="wrap_content"
60120
android:layout_height="wrap_content"
61-
android:contentDescription="@string/app_settings"
62-
app:icon="@drawable/ic_navigation_menu"
121+
android:contentDescription="@string/action_add_profile"
122+
app:icon="@drawable/ic_baseline_add_24"
63123
app:iconTint="?attr/colorPrimary" />
64124
</LinearLayout>
65125

66-
<!-- Add a subscription / import a config -->
67126
<com.google.android.material.card.MaterialCardView
127+
android:id="@+id/empty_card"
68128
android:layout_width="match_parent"
69129
android:layout_height="wrap_content"
130+
android:visibility="gone"
70131
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
71132
app:cardCornerRadius="24dp"
72-
app:cardElevation="2dp"
73-
app:strokeWidth="0dp">
133+
app:cardElevation="0dp"
134+
app:strokeWidth="0dp"
135+
tools:visibility="visible">
74136

75-
<LinearLayout
137+
<TextView
138+
android:id="@+id/tv_empty"
76139
android:layout_width="match_parent"
77140
android:layout_height="wrap_content"
78-
android:orientation="vertical"
79-
android:padding="16dp">
80-
81-
<com.google.android.material.textfield.TextInputLayout
82-
android:layout_width="match_parent"
83-
android:layout_height="wrap_content"
84-
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
85-
86-
<com.google.android.material.textfield.TextInputEditText
87-
android:id="@+id/et_sub_url"
88-
android:layout_width="match_parent"
89-
android:layout_height="wrap_content"
90-
android:hint="@string/hint_subscription_url"
91-
android:inputType="textUri"
92-
android:maxLines="1" />
93-
</com.google.android.material.textfield.TextInputLayout>
94-
95-
<com.google.android.material.textfield.TextInputLayout
96-
android:layout_width="match_parent"
97-
android:layout_height="wrap_content"
98-
android:layout_marginTop="8dp"
99-
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
100-
101-
<com.google.android.material.textfield.TextInputEditText
102-
android:id="@+id/et_name"
103-
android:layout_width="match_parent"
104-
android:layout_height="wrap_content"
105-
android:hint="@string/hint_profile_name"
106-
android:inputType="text"
107-
android:maxLines="1" />
108-
</com.google.android.material.textfield.TextInputLayout>
109-
110-
<com.google.android.material.button.MaterialButton
111-
android:id="@+id/btn_add_sub"
112-
android:layout_width="match_parent"
113-
android:layout_height="wrap_content"
114-
android:layout_marginTop="8dp"
115-
android:text="@string/action_add_subscription" />
116-
117-
<com.google.android.material.button.MaterialButton
118-
android:id="@+id/btn_import_clipboard"
119-
style="@style/Widget.Material3.Button.OutlinedButton"
120-
android:layout_width="match_parent"
121-
android:layout_height="wrap_content"
122-
android:text="@string/action_import_clipboard" />
123-
</LinearLayout>
141+
android:gravity="center"
142+
android:padding="28dp"
143+
android:text="@string/profiles_empty"
144+
android:textColor="?attr/colorOnSurfaceVariant"
145+
android:textSize="14sp" />
124146
</com.google.android.material.card.MaterialCardView>
125147

126-
<TextView
127-
android:layout_width="wrap_content"
128-
android:layout_height="wrap_content"
129-
android:layout_marginTop="20dp"
130-
android:layout_marginStart="4dp"
131-
android:layout_marginBottom="4dp"
132-
android:text="@string/profiles_header"
133-
android:textColor="?attr/colorOnSurface"
134-
android:textSize="16sp"
135-
android:textStyle="bold" />
136-
137-
<TextView
138-
android:id="@+id/tv_empty"
139-
android:layout_width="match_parent"
140-
android:layout_height="wrap_content"
141-
android:layout_marginStart="4dp"
142-
android:text="@string/profiles_empty"
143-
android:textColor="?attr/colorOnSurfaceVariant"
144-
android:visibility="gone" />
145-
146148
<androidx.recyclerview.widget.RecyclerView
147149
android:id="@+id/rv_profiles"
148150
android:layout_width="match_parent"

0 commit comments

Comments
 (0)