Parcourir la source

feat: show subscription

metacubex il y a 3 ans
Parent
commit
e06c901bf5

+ 0 - 9
.idea/codeStyles/Project.xml

@@ -20,15 +20,6 @@
           <package name="io.ktor" alias="false" withSubpackages="true" />
         </value>
       </option>
-      <option name="PACKAGES_IMPORT_LAYOUT">
-        <value>
-          <package name="" alias="false" withSubpackages="true" />
-          <package name="java" alias="false" withSubpackages="true" />
-          <package name="javax" alias="false" withSubpackages="true" />
-          <package name="kotlin" alias="false" withSubpackages="true" />
-          <package name="" alias="true" withSubpackages="true" />
-        </value>
-      </option>
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
     <codeStyleSettings language="XML">

+ 8 - 0
design/src/main/java/com/github/kr328/clash/design/util/I18n.kt

@@ -65,4 +65,12 @@ fun Long.toBytesString(): String {
         else ->
             "$this Bytes"
     }
+}
+
+fun Double.toProgress(): Int {
+    return this.toInt()
+}
+fun Long.toDateStr(): String {
+    val simpleDateFormat =SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+    return simpleDateFormat.format(Date(this*1000))
 }

+ 12 - 0
design/src/main/res/drawable/bg_b.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="?attr/colorSurface" />
+            <corners
+                android:radius="8dp"
+                />
+        </shape>
+    </item>
+
+</layer-list>

+ 44 - 7
design/src/main/res/layout/adapter_profile.xml

@@ -13,16 +13,18 @@
         <variable
             name="menu"
             type="android.view.View.OnClickListener" />
-
+        <import type="android.view.View" />
         <import type="com.github.kr328.clash.design.util.I18nKt" />
         <import type="com.github.kr328.clash.design.util.IntervalKt" />
     </data>
 
     <RelativeLayout
+        android:elevation="5dp"
         android:id="@+id/root_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:background="?attr/selectableItemBackground"
+        android:layout_margin="5dp"
+        android:background="@drawable/bg_b"
         android:clickable="true"
         android:focusable="true"
         android:minHeight="@dimen/item_min_height"
@@ -61,6 +63,43 @@
                 android:layout_marginTop="@dimen/item_text_margin"
                 android:text="@{profile.pending ? @string/format_type_unsaved(I18nKt.toString(profile.type, context)) : I18nKt.toString(profile.type, context)}"
                 android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" />
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="horizontal"
+                >
+
+                <TextView
+                    android:textSize="12sp"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:visibility="@{profile.download ==0 ? View.GONE : View.VISIBLE}"
+                    android:text='@{profile.download >0 ?I18nKt.toBytesString(profile.download+profile.upload)+"/" :""}'
+                    android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
+
+                <TextView
+                    android:textSize="12sp"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:visibility="@{profile.download ==0 ? View.GONE : View.VISIBLE}"
+                    android:text='@{profile.download >0 ?I18nKt.toBytesString(profile.total) : ""}'
+                    android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
+            </LinearLayout>
+            <TextView
+                android:textSize="12sp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:visibility="@{profile.expire==0 ? View.GONE : View.VISIBLE}"
+                android:text='@{profile.expire>0 ? I18nKt.toDateStr(profile.expire):""}'
+                android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
+            <ProgressBar
+                style="?android:attr/progressBarStyleHorizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:max="1000"
+                android:visibility="@{profile.download==0 ? View.GONE : View.VISIBLE}"
+                android:progress="@{profile.download>0 ?I18nKt.toProgress ((profile.download+profile.upload)/(profile.total/1000)) :0}"
+                />
         </LinearLayout>
 
         <TextView
@@ -74,12 +113,10 @@
             android:textAppearance="@style/TextAppearance.MaterialComponents.Tooltip" />
 
         <View
-            android:layout_width="@dimen/divider_size"
-            android:layout_height="wrap_content"
-            android:layout_centerVertical="true"
+            android:layout_width="2dp"
+            android:layout_height="match_parent"
             android:layout_toStartOf="@id/menu_view"
-            android:background="?attr/colorControlHighlight"
-            android:minHeight="@{@dimen/item_tailing_component_size * 1.5f}" />
+            android:background="@color/color_clash_dark" />
 
         <FrameLayout
             android:id="@+id/menu_view"

+ 5 - 0
service/build.gradle.kts

@@ -19,6 +19,11 @@ dependencies {
     implementation(libs.androidx.room.ktx)
     implementation(libs.kaidl.runtime)
     implementation(libs.rikkax.multiprocess)
+    implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
+
+    // define any required OkHttp artifacts without version
+    implementation("com.squareup.okhttp3:okhttp")
+    implementation("com.squareup.okhttp3:logging-interceptor")
 }
 
 afterEvaluate {

+ 104 - 1
service/src/main/java/com/github/kr328/clash/service/ProfileManager.kt

@@ -2,6 +2,7 @@ package com.github.kr328.clash.service
 
 import android.content.Context
 import com.github.kr328.clash.service.data.Database
+import com.github.kr328.clash.service.data.Imported
 import com.github.kr328.clash.service.data.ImportedDao
 import com.github.kr328.clash.service.data.Pending
 import com.github.kr328.clash.service.data.PendingDao
@@ -13,10 +14,13 @@ import com.github.kr328.clash.service.util.directoryLastModified
 import com.github.kr328.clash.service.util.generateProfileUUID
 import com.github.kr328.clash.service.util.importedDir
 import com.github.kr328.clash.service.util.pendingDir
+import com.github.kr328.clash.service.util.sendProfileChanged
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
 import java.io.FileNotFoundException
 import java.util.*
 
@@ -40,6 +44,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
             type = type,
             source = source,
             interval = 0,
+            upload = 0,
+            total = 0,
+            download = 0,
+            expire = 0,
         )
 
         PendingDao().insert(pending)
@@ -68,6 +76,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
             type = Profile.Type.File,
             source = imported.source,
             interval = imported.interval,
+            upload = imported.upload,
+            total = imported.total,
+            download = imported.download,
+            expire = imported.expire,
         )
 
         cloneImportedFiles(uuid, newUUID)
@@ -93,13 +105,21 @@ class ProfileManager(private val context: Context) : IProfileManager,
                     type = imported.type,
                     source = source,
                     interval = interval,
+                    upload = 0,
+                    total = 0,
+                    download = 0,
+                    expire = 0,
                 )
             )
         } else {
             val newPending = pending.copy(
                 name = name,
                 source = source,
-                interval = interval
+                interval = interval,
+                upload = 0,
+                total = 0,
+                download = 0,
+                expire = 0,
             )
 
             PendingDao().update(newPending)
@@ -108,6 +128,81 @@ class ProfileManager(private val context: Context) : IProfileManager,
 
     override suspend fun update(uuid: UUID) {
         scheduleUpdate(uuid, true)
+        ImportedDao().queryByUUID(uuid)?.let {
+            if (it.type == Profile.Type.Url) {
+                updateFlow(it)
+            }
+        }
+    }
+
+    suspend fun updateFlow(old: Imported) {
+        val client = OkHttpClient()
+        try {
+            val request = Request.Builder()
+                .url(old.source)
+                .header("User-Agent", "ClashforWindows/0.19.23")
+                .build()
+
+            client.newCall(request).execute().use { response ->
+                if (!response.isSuccessful || response.headers["subscription-userinfo"] == null) return
+
+                var upload: Long = 0
+                var download: Long = 0
+                var total: Long = 0
+                var expire: Long = 0
+
+                val userinfo = response.headers["subscription-userinfo"]
+                if (response.isSuccessful && userinfo != null) {
+
+                    val flags = userinfo.split(";")
+                    for (flag in flags) {
+                        val info = flag.split("=")
+                        when {
+                            info[0].contains("upload") -> upload =
+                                info[1].toLong()
+
+                            info[0].contains("download") -> download =
+                                info[1].toLong()
+
+                            info[0].contains("total") -> total =
+                                info[1].toLong()
+
+                            info[0].contains("expire") -> {
+                                if (info[1].isNotEmpty()) {
+                                    expire = info[1].toLong()
+                                }
+                            }
+                        }
+                    }
+                }
+
+                val new = Imported(
+                    old.uuid,
+                    old.name,
+                    old.type,
+                    old.source,
+                    old.interval,
+                    upload,
+                    download,
+                    total,
+                    expire,
+                    old?.createdAt ?: System.currentTimeMillis()
+                )
+
+                if (old != null) {
+                    ImportedDao().update(new)
+                } else {
+                    ImportedDao().insert(new)
+                }
+
+                PendingDao().remove(new.uuid)
+                context.sendProfileChanged(new.uuid)
+                // println(response.body!!.string())
+            }
+
+        } catch (e: Exception) {
+            System.out.println(e)
+        }
     }
 
     override suspend fun commit(uuid: UUID, callback: IFetchObserver?) {
@@ -163,6 +258,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
         val type = pending?.type ?: imported?.type ?: return null
         val source = pending?.source ?: imported?.source ?: return null
         val interval = pending?.interval ?: imported?.interval ?: return null
+        val upload = pending?.upload ?: imported?.upload ?: return null
+        val download = pending?.download ?: imported?.download ?: return null
+        val total = pending?.total ?: imported?.total ?: return null
+        val expire = pending?.expire ?: imported?.expire ?: return null
 
         return Profile(
             uuid,
@@ -171,6 +270,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
             source,
             active != null && imported?.uuid == active,
             interval,
+            upload,
+            download,
+            total,
+            expire,
             resolveUpdatedAt(uuid),
             imported != null,
             pending != null

+ 91 - 21
service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt

@@ -19,6 +19,8 @@ import kotlinx.coroutines.NonCancellable
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
 import java.util.*
 import java.util.concurrent.TimeUnit
 
@@ -65,28 +67,93 @@ object ProfileProcessor {
                             .copyRecursively(context.importedDir.resolve(snapshot.uuid.toString()))
 
                         val old = ImportedDao().queryByUUID(snapshot.uuid)
-
-                        val new = Imported(
-                            snapshot.uuid,
-                            snapshot.name,
-                            snapshot.type,
-                            snapshot.source,
-                            snapshot.interval,
-                            old?.createdAt ?: System.currentTimeMillis()
-                        )
-
-                        if (old != null) {
-                            ImportedDao().update(new)
-                        } else {
-                            ImportedDao().insert(new)
+                        var upload: Long = 0
+                        var download: Long = 0
+                        var total: Long = 0
+                        var expire: Long = 0
+                        if (snapshot?.type == Profile.Type.Url) {
+                            val client = OkHttpClient()
+                            val request = Request.Builder()
+                                .url(snapshot.source)
+                                .header("User-Agent", "ClashforWindows/0.19.23")
+                                .build()
+
+                            client.newCall(request).execute().use { response ->
+                                val userinfo = response.headers["subscription-userinfo"]
+                                if (response.isSuccessful && userinfo != null) {
+                                    val flags = userinfo.split(";")
+                                    for (flag in flags) {
+                                        val info = flag.split("=")
+                                        when {
+                                            info[0].contains("upload") -> upload =
+                                                info[1].toLong()
+
+                                            info[0].contains("download") -> download =
+                                                info[1].toLong()
+
+                                            info[0].contains("total") -> total =
+                                                info[1].toLong()
+
+                                            info[0].contains("expire") -> {
+                                                if (info[1].isNotEmpty()) {
+                                                    expire = info[1].toLong()
+                                                }
+                                            }
+                                        }
+                                    }
+
+                                }
+                                val new = Imported(
+                                    snapshot.uuid,
+                                    snapshot.name,
+                                    snapshot.type,
+                                    snapshot.source,
+                                    snapshot.interval,
+                                    upload,
+                                    download,
+                                    total,
+                                    expire,
+                                    old?.createdAt ?: System.currentTimeMillis()
+                                )
+                                if (old != null) {
+                                    ImportedDao().update(new)
+                                } else {
+                                    ImportedDao().insert(new)
+                                }
+
+                                PendingDao().remove(snapshot.uuid)
+
+                                context.pendingDir.resolve(snapshot.uuid.toString())
+                                    .deleteRecursively()
+
+                                context.sendProfileChanged(snapshot.uuid)
+                            }
+                        } else if (snapshot?.type == Profile.Type.File) {
+                            val new = Imported(
+                                snapshot.uuid,
+                                snapshot.name,
+                                snapshot.type,
+                                snapshot.source,
+                                snapshot.interval,
+                                upload,
+                                download,
+                                total,
+                                expire,
+                                old?.createdAt ?: System.currentTimeMillis()
+                            )
+                            if (old != null) {
+                                ImportedDao().update(new)
+                            } else {
+                                ImportedDao().insert(new)
+                            }
+
+                            PendingDao().remove(snapshot.uuid)
+
+                            context.pendingDir.resolve(snapshot.uuid.toString())
+                                .deleteRecursively()
+
+                            context.sendProfileChanged(snapshot.uuid)
                         }
-
-                        PendingDao().remove(snapshot.uuid)
-
-                        context.pendingDir.resolve(snapshot.uuid.toString())
-                            .deleteRecursively()
-
-                        context.sendProfileChanged(snapshot.uuid)
                     }
                 }
             }
@@ -181,10 +248,13 @@ object ProfileProcessor {
         when {
             name.isBlank() ->
                 throw IllegalArgumentException("Empty name")
+
             source.isEmpty() && type != Profile.Type.File ->
                 throw IllegalArgumentException("Invalid url")
+
             source.isNotEmpty() && scheme != "https" && scheme != "http" && scheme != "content" ->
                 throw IllegalArgumentException("Unsupported url $source")
+
             interval != 0L && TimeUnit.MILLISECONDS.toMinutes(interval) < 15 ->
                 throw IllegalArgumentException("Invalid interval")
         }

+ 4 - 0
service/src/main/java/com/github/kr328/clash/service/data/Imported.kt

@@ -14,5 +14,9 @@ data class Imported(
     @ColumnInfo(name = "type") val type: Profile.Type,
     @ColumnInfo(name = "source") val source: String,
     @ColumnInfo(name = "interval") val interval: Long,
+    @ColumnInfo(name = "upload") val upload: Long,
+    @ColumnInfo(name = "download") val download: Long,
+    @ColumnInfo(name = "total") val total: Long,
+    @ColumnInfo(name = "expire") val expire: Long,
     @ColumnInfo(name = "createdAt") val createdAt: Long,
 )

+ 4 - 0
service/src/main/java/com/github/kr328/clash/service/data/Pending.kt

@@ -14,5 +14,9 @@ data class Pending(
     @ColumnInfo(name = "type") val type: Profile.Type,
     @ColumnInfo(name = "source") val source: String,
     @ColumnInfo(name = "interval") val interval: Long,
+    @ColumnInfo(name = "upload") val upload: Long,
+    @ColumnInfo(name = "download") val download: Long,
+    @ColumnInfo(name = "total") val total: Long,
+    @ColumnInfo(name = "expire") val expire: Long,
     @ColumnInfo(name = "createdAt") val createdAt: Long = System.currentTimeMillis(),
 )

+ 3 - 1
service/src/main/java/com/github/kr328/clash/service/data/migrations/LegacyMigration.kt

@@ -92,7 +92,8 @@ private suspend fun migrationFromLegacy234(
                 type = newType,
                 source = if (newType != Profile.Type.File) cursor.getString(uri) else "",
                 interval = if (version == 2) intervalValue * 1000 else intervalValue,
-            )
+                0,0,0,0
+                )
 
             val base = context.pendingDir.resolve(pending.uuid.toString())
 
@@ -165,6 +166,7 @@ private suspend fun migrationFromLegacy1(context: Context, legacy: SQLiteDatabas
                 type = newType,
                 source = source,
                 interval = 0,
+                0,0,0,0
             )
 
             val base = context.pendingDir.resolve(pending.uuid.toString())

+ 2 - 1
service/src/main/java/com/github/kr328/clash/service/document/Picker.kt

@@ -133,7 +133,8 @@ class Picker(private val context: Context) {
                 imported.name,
                 imported.type,
                 imported.source,
-                imported.interval
+                imported.interval,
+                0,0,0,0
             )
         )
 

+ 5 - 0
service/src/main/java/com/github/kr328/clash/service/model/Profile.kt

@@ -18,6 +18,11 @@ data class Profile(
     val source: String,
     val active: Boolean,
     val interval: Long,
+    val upload: Long,
+    var download: Long,
+    val total: Long,
+    val expire: Long,
+
 
     val updatedAt: Long,
     val imported: Boolean,