68 Commits

Author SHA1 Message Date
AnranYus e9c1f9d176 Update Compose 2024-06-29 21:58:26 +08:00
AnranYus 5d84fc4df2 style: string 2024-06-29 21:12:19 +08:00
AnranYus f706720a37 feat: use wallpaper 2024-06-29 20:55:32 +08:00
AnranYus ab82ef5ff6 fix: list span 2024-06-29 14:55:29 +08:00
AnranYus b6fe7f1561 refactor: 拆分 2024-06-17 17:21:58 +08:00
AnranYus 344f587ca7 fix: horizontal space 2024-06-16 12:51:54 +08:00
AnranYus 7d489e5d51 fix: can not load more 2024-06-16 12:35:35 +08:00
AnranYus 1cf4d6cb3d feat: share 2024-06-16 02:48:31 +08:00
AnranYus 42c955be6d refactor: jetpack compose (4/n) 2024-06-16 02:01:29 +08:00
AnranYus b7f90e0848 fix: material3 to list item 2024-06-15 23:48:47 +08:00
AnranYus 329da172e8 refactor: searchBar and list 2024-06-15 23:03:46 +08:00
AnranYus 1d208099f9 fix: change searchbar 2024-06-15 18:54:21 +08:00
AnranYus a56d021a4a fix: SearchBar padding 2024-06-15 03:22:31 +08:00
AnranYus b7efc85fa9 fix: SearchBar text color 2024-06-15 03:09:46 +08:00
AnranYus c4f9dbb781 refactor: remove redundant definitions 2024-06-15 03:03:19 +08:00
AnranYus f66fa1131a fix: can not show download result 2024-06-15 03:00:42 +08:00
AnranYus 5baa502363 feat: search 2024-06-15 02:52:23 +08:00
AnranYus 6b71b31dfc feat: support zoom 2024-06-14 17:54:26 +08:00
AnranYus 821c6ea0d6 Remove github workflow 2024-06-14 17:17:29 +08:00
AnranYus 87c6ecf74a refactor: jetpack compose (3/n) 2024-06-14 16:36:43 +08:00
AnranYus 65c911585c fix: remove string 2024-06-14 01:11:24 +08:00
AnranYus 1398fcc658 refactor: add build types 2024-06-14 01:09:18 +08:00
AnranYus 6b79f68d92 refactor: jetpack compose(2/n) 2024-06-14 01:05:11 +08:00
AnranYus dee0b7e73d Update gradle 2024-06-13 23:25:37 +08:00
AnranYus b180dbb138 fix:crash 2024-06-13 23:25:22 +08:00
AnranYus 669770d1df refactor: jetpack compose(1/n) 2024-06-13 18:50:59 +08:00
AnranYus e1a9534e7a refactor: remove image tag 2024-03-10 23:17:19 +08:00
AnranYus 18f2583007 update 2024-03-10 23:00:14 +08:00
AnranYus 11e2572e09 Update README.md 2024-03-03 18:29:55 +08:00
AnranYus a0600088a7 Merge pull request #3 from XDH2022/dev
fix:reload
2024-03-02 20:45:49 +08:00
86152 6296ef6348 fix:reload 2024-03-02 20:37:01 +08:00
Anran c28b73edf8 feat: Themed icons 2023-11-24 23:07:45 +08:00
Anran 429943b89d fix: add chinese 2023-11-24 22:45:30 +08:00
Anran c7be917ea3 feat: add tags dialog 2023-11-20 23:30:50 +08:00
AnranYus 90ac4169ff style: let settings return 2023-11-11 15:02:07 +08:00
AnranYus db7682f298 style: Redefine package 2023-11-11 15:00:55 +08:00
AnranYus b702e95f40 refactor: To material3 2023-11-06 23:16:58 +08:00
AnranYus e3336b0458 feat: Use material search view & recyclerview adapter handler is activity 2023-11-04 19:00:41 +08:00
AnranYus b386e87bc8 fix: Can load more content 2023-11-04 17:24:45 +08:00
AnranYus 6edd9e6512 fix: safe model 2023-10-21 15:49:44 +08:00
AnranYus 091a891394 style: Maintain architectural boundaries 2023-10-21 15:31:12 +08:00
AnranYus b9de8535c1 fix: github action 2023-10-20 19:26:53 +08:00
AnranYus 9b8832b8a6 fix: Add permission with notification 2023-10-20 19:26:37 +08:00
AnranYus 72a44d5a48 Revert "fix: Update github action jdk version"
This reverts commit a846c84805.
2023-10-20 19:21:55 +08:00
AnranYus a846c84805 fix: Update github action jdk version 2023-10-20 19:17:03 +08:00
AnranYus 9ef7dda7dc fix: search 2023-10-20 19:14:34 +08:00
AnranYus 54a8ece487 style: change some view 2023-10-20 18:52:54 +08:00
AnranYus d83082211e fix: Avoid giving away context 2023-10-20 16:02:39 +08:00
AnranYus 25046d22a8 refactor:Use viewModel 2023-10-20 15:27:14 +08:00
AnranYus bbd4c3b7d1 style: rename 2023-10-19 17:14:29 +08:00
AnranYus d2c903ca41 style: move function 2023-10-19 17:06:20 +08:00
AnranYus 816eaa7ecf style: Adaptive adjustment 2023-10-19 16:55:21 +08:00
anrannn 1c48892cb4 Use new authority id 2022-12-05 00:45:17 +08:00
anrannn 0637278a17 Modify! Unified naming format 2022-11-05 23:13:33 +08:00
anrannn 415139d67d Modify! Move the preferences config from com.lsper.view_preferences to com.lsp.view_preferences 2022-11-05 23:07:42 +08:00
anrannn c13215a6dd Modify!Remove duplicate path judgment 2022-11-05 23:04:03 +08:00
anrannn c82509b320 Modify!drop callback param 2022-11-05 22:52:14 +08:00
anrannn ad5ef05c60 Use new applicationId 2022-11-03 17:19:39 +08:00
anrannn 8736940b9f Adapt icon from color 2022-11-02 19:03:43 +08:00
anrannn e925b14b47 Drop unused resource. 2022-11-02 18:20:14 +08:00
anrannn f28d36f0f4 Drop!Author: Vague information is used 2022-11-02 17:30:35 +08:00
anrannn 3998658726 Modify!Layout: Modify sample image layout 2022-11-02 17:25:02 +08:00
anrannn 0c9c3958fb Drop!Login: Temporarily unneeded features. 2022-11-02 15:47:58 +08:00
anrannn 7a3b7947fb Update! 2022-10-23 19:06:45 +08:00
anrannn 58f39bd610 Adapt SDK 33 2022-10-23 18:48:33 +08:00
anrannn fe605d0f89 Delete variable: searchTag, new logic in search 2022-10-23 18:41:01 +08:00
anrannn 1870bc71c3 New search bar 2022-10-23 13:55:48 +08:00
anrannn 3756900873 Revert "Use dynamic action bar"
This reverts commit e13cff7b70.
2022-10-22 17:42:42 +08:00
101 changed files with 1524 additions and 2453 deletions
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
- name: set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
+2 -2
View File
@@ -7,10 +7,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
- name: set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
-25
View File
@@ -1,25 +0,0 @@
name: Releases
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
- name: Archive output results
uses: actions/upload-artifact@v3
with:
name: debug-outputs
path: app/build/outputs/apk/debug
-12
View File
@@ -1,13 +1 @@
# YandView
Android平台的yande.re和konachan非官方客户端
- 能干什么?
- 搜索和浏览色图(废话)
- 收藏特定的标签或ID(仅限本地存储)
- 支持R18过滤(基于网站官方的评级)
# Build
In Terminal:
cd [ProjectPath]
gradlew assembleDebug
+57 -20
View File
@@ -1,26 +1,36 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs.kotlin'
id "com.google.devtools.ksp"
}
android {
compileSdkVersion 33
buildToolsVersion "30.0.3"
compileSdkVersion = 34
defaultConfig {
applicationId "com.lsper.view"
applicationId "moe.uni.moe"
minSdkVersion 29
targetSdkVersion 33
versionCode 221022
versionName "1.2.7"
targetSdkVersion 34
versionCode 240629
versionName "2.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
debug {
debuggable true
minifyEnabled false
shrinkResources false
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
@@ -30,24 +40,51 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
namespace 'moe.uni.view'
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.1'
}
packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation("com.google.android.material:material:1.5.0")
implementation 'jp.wasabeef:recyclerview-animators:4.0.2'
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation ('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0')
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2'
implementation 'androidx.activity:activity-compose:1.9.0'
implementation platform('androidx.compose:compose-bom:2024.06.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material'
androidTestImplementation platform('androidx.compose:compose-bom:2024.06.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
implementation("io.coil-kt:coil-compose:2.6.0")
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2")
implementation ('com.squareup.retrofit2:retrofit:2.9.0')
implementation ('com.squareup.retrofit2:converter-gson:2.9.0')
implementation 'com.github.bumptech.glide:glide:4.12.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+13 -24
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lsp.view">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".YandViewApplication"
@@ -12,10 +12,20 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
<activity
android:name=".ui.compose.PostActivity"
android:exported="true"
android:theme="@style/Theme.Material3.DayNight.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.lsper.view.fileprovider"
android:authorities="moe.uni.view.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
@@ -24,32 +34,11 @@
</meta-data>
</provider>
<activity
android:name=".activity.favtag.FavTagActivity"
android:exported="false" />
<activity
android:name=".activity.setting.SettingsActivity"
android:exported="false"
android:label="@string/title_activity_settings" />
<service
android:name=".service.DownloadService"
android:enabled="true"
android:exported="true" />
<activity android:name=".activity.login.LoginActivity" />
<activity
android:name=".activity.pic.PicActivity"
android:configChanges="uiMode"/>
<activity
android:name=".activity.main.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -1,58 +0,0 @@
package com.lsp.view
import android.app.Application
import androidx.lifecycle.DefaultLifecycleObserver
import android.content.ServiceConnection
import android.content.ComponentName
import android.content.Context
import android.os.IBinder
import com.lsp.view.service.DownloadService.DownloadBinder
import com.google.android.material.color.DynamicColors
import android.content.Intent
import com.lsp.view.service.DownloadService
import androidx.lifecycle.LifecycleOwner
class YandViewApplication : Application(), DefaultLifecycleObserver {
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
downloadBinder = iBinder as DownloadBinder
}
override fun onServiceDisconnected(componentName: ComponentName) {}
}
override fun onCreate() {
super<Application>.onCreate()
context = applicationContext
DynamicColors.applyToActivitiesIfAvailable(this)
val serviceIntent = Intent(this, DownloadService::class.java)
bindService(serviceIntent, connection, BIND_AUTO_CREATE)
statusBarHeight()
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
unbindService(connection)
}
//计算状态栏高度
fun statusBarHeight(): Int {
var height = 0
val resourceId = resources.getIdentifier(
"status_bar_height",
"dimen",
"android"
)
if (resourceId > 0) {
height = resources.getDimensionPixelSize(resourceId)
}
return height
}
companion object {
var context: Context? = null
private set
var downloadBinder: DownloadBinder? = null
private set
}
}
@@ -1,42 +0,0 @@
package com.lsp.view.activity
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
transparentStatusBar()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
//设置导航栏透明
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
}
private fun transparentStatusBar(){
// 设置状态栏颜色为透明
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
//状态栏反色
val controller = ViewCompat.getWindowInsetsController(window.decorView)
controller?.isAppearanceLightStatusBars = !isDarkMode()
}
/**
* 获取当前是否为深色模式
* 深色模式的值为:0x21
* 浅色模式的值为:0x11
* @return true 为是深色模式 false为不是深色模式
*/
private fun isDarkMode(): Boolean {
return resources.configuration.uiMode == 0x21
}
}
@@ -1,83 +0,0 @@
package com.lsp.view.activity.favtag
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.*
import com.google.android.material.appbar.AppBarLayout
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.lsp.view.YandViewApplication
import com.lsp.view.R
import com.lsp.view.activity.BaseActivity
import com.lsp.view.activity.main.MainActivity
import com.lsp.view.bean.Tags
class FavTagActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fav_tag)
//Toolbar相关
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val appbar = findViewById<AppBarLayout>(R.id.appbar)
val nowHeight = appbar.layoutParams.height
appbar.layoutParams.height = (application as YandViewApplication).statusBarHeight()+nowHeight
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
val recyclerViewTag = findViewById<RecyclerView>(R.id.tag_recy)
recyclerViewTag.layoutManager = layoutManager()
val tagsArraySp = getSharedPreferences("tags_sp", Context.MODE_PRIVATE)
val array = tagsArraySp.getString("array", null)
var favTagList: ArrayList<Tags> = ArrayList()
if (array != null) {
//有收藏内容
val tagListType = object : TypeToken<ArrayList<Tags>>() {}.type
favTagList = Gson().fromJson(array, tagListType)
}
val favTagAdapter = FavTagAdapter(favTagList, this)
favTagAdapter.setOnItemClickListener(object : FavTagAdapter.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
val intent = Intent(this@FavTagActivity, MainActivity::class.java)
intent.putExtra("searchTag", favTagList[position].tag)
startActivity(intent)
}
})
recyclerViewTag.adapter = favTagAdapter
}
private fun layoutManager(): FlexboxLayoutManager {
val manager = object : FlexboxLayoutManager(this, FlexDirection.ROW, FlexWrap.WRAP) {
override fun canScrollVertically(): Boolean {
return false
}
}
manager.flexWrap = FlexWrap.WRAP
manager.flexDirection = FlexDirection.ROW
manager.alignItems = AlignItems.CENTER
manager.justifyContent = JustifyContent.FLEX_START
return manager
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
finish()
}
}
return super.onOptionsItemSelected(item)
}
}
@@ -1,72 +0,0 @@
package com.lsp.view.activity.favtag
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.lsp.view.R
import com.lsp.view.bean.Tags
class FavTagAdapter(val tagList: ArrayList<Tags>, val context: Context) :
RecyclerView.Adapter<FavTagAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fav_tag = view.findViewById<TextView>(R.id.fav_tag)
}
private lateinit var mOnItemClickListener: OnItemClickListener
interface OnItemClickListener {
fun onItemClick(view: View, position: Int)
}
fun setOnItemClickListener(mOnItemClickListener: OnItemClickListener) {
this.mOnItemClickListener = mOnItemClickListener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.fav_tag_item_layout, parent, false)
val viewHolder = ViewHolder(view)
viewHolder.fav_tag.setOnLongClickListener {
val position = viewHolder.adapterPosition
removeData(position)
true
}
return viewHolder
}
fun removeData(position: Int) {
tagList.removeAt(position)
notifyItemRangeInserted(0, tagList.size)
val tagsArraySp = context.getSharedPreferences("tags_sp", Context.MODE_PRIVATE)
val tagsArrayJson = tagsArraySp.getString("array", null)
val tagListType = object : TypeToken<ArrayList<Tags>>() {}.type
val tagArray: ArrayList<Tags> = Gson().fromJson(tagsArrayJson, tagListType)
tagArray.removeAt(position)
tagsArraySp.edit().putString("array", Gson().toJson(tagArray)).apply()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val tag = tagList[position].tag
holder.fav_tag.text = tag
holder.fav_tag.setOnClickListener {
mOnItemClickListener.onItemClick(it, position)
}
}
override fun getItemCount(): Int {
return tagList.size
}
}
@@ -1,57 +0,0 @@
package com.lsp.view.activity.login
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.lsp.view.R
import com.lsp.view.activity.main.MainActivity
import okhttp3.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val login = findViewById<FloatingActionButton>(R.id.login)
login.setOnClickListener {
val username = findViewById<EditText>(R.id.username)
val password = findViewById<EditText>(R.id.password)
val sPassword = "choujin-steiner--${password.text}--"
val hash1Password = StrToHash1.shaEncrypt(sPassword)
thread {
conn(username.text.toString(), hash1Password)
}
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}
fun conn(username: String, hash1: String): String {
val cookies = ArrayList<Cookie>()
val client = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.cookieJar(object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return cookies
}
})
.build()
val request = Request.Builder()
.url("https://yande.re/?username=$username&password=$hash1")
.build()
return client.newCall(request).execute().body()!!.string()
}
}
@@ -1,36 +0,0 @@
package com.lsp.view.activity.login;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class StrToHash1 {
public static String shaEncrypt(String strSrc) {
MessageDigest md = null;
String strDes = null;
byte[] bt = strSrc.getBytes();
try {
md = MessageDigest.getInstance("SHA-1");
md.update(bt);
strDes = bytes2Hex(md.digest());
} catch (NoSuchAlgorithmException e) {
return null;
}
return strDes;
}
private static String bytes2Hex(byte[] bts) {
StringBuilder des = new StringBuilder();
String tmp = null;
for (byte bt : bts) {
tmp = Integer.toHexString(bt & 0xFF);
if (tmp.length() == 1) {
des.append("0");
}
des.append(tmp);
}
return des.toString();
}
}
@@ -1,343 +0,0 @@
package com.lsp.view.activity.main
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Intent
import android.os.*
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.LinearLayout
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat.Type.ime
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import com.lsp.view.YandViewApplication
import com.lsp.view.R
import com.lsp.view.activity.BaseActivity
import com.lsp.view.activity.favtag.FavTagActivity
import com.lsp.view.activity.model.MainActivityModelImpl
import com.lsp.view.activity.setting.SettingsActivity
import com.lsp.view.bean.Post_yand
import com.lsp.view.util.CallBackStatus
class MainActivity : BaseActivity() {
private var searchTag: String? = null
private lateinit var search: EditText
private lateinit var searchBar: LinearLayout
private var shortAnnotationDuration: Int = 0
private var nowPage = 1
private val adapter: PostAdapter = PostAdapter(this, ArrayList())
private var username: String? = ""
private lateinit var sourceUrlArray: Array<String>
private lateinit var sourceNameArray: Array<String>
private var nowSourceName: String? = null
val TAG = javaClass.simpleName
private lateinit var layoutManager: RecyclerView.LayoutManager
private var safeMode: Boolean = true //安全模式
private lateinit var recyclerView: RecyclerView
private var barShow = false
private var tags: String? = ""
private val ISREFRESH = 1
private val ISADDDATA = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val appbar = findViewById<AppBarLayout>(R.id.appbar)
val nowHeight = appbar.layoutParams.height
appbar.layoutParams.height = (application as YandViewApplication).statusBarHeight()+nowHeight
supportActionBar?.setDisplayShowTitleEnabled(false)
//到达底部,加载更多数据
adapter.setLoadMoreListener(object : PostAdapter.OnLoadMoreListener {
//重写接口
override fun loadMore(position: Int) {
loadData(tags,++nowPage,ISADDDATA)
}
})
//初始化数据
sourceNameArray = resources.getStringArray(R.array.pic_source)
sourceUrlArray = resources.getStringArray(R.array.url_source)
val configSp = getSharedPreferences("com.lsper.view_preferences", 0)
if (configSp.getString("sourceName", null) == null) {
configSp.edit().putString("sourceName", "yande.re").apply()
configSp.edit().putString("type", "0").apply()
}
nowSourceName = configSp.getString("sourceName","yande.re")
safeMode = configSp.getBoolean("safe_mode",true)
//横屏逻辑
layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
recyclerView = findViewById(R.id.recyclerview)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
//收藏Tag
val swipeRefreshLayout =
findViewById<androidx.swiperefreshlayout.widget.SwipeRefreshLayout>(
R.id.swipeRefreshLayout
)
val drawerLayout = findViewById<DrawerLayout>(R.id.drawerLayout)
search = findViewById(R.id.search)
//快捷搜索tag 来自PicActivity
searchTag = intent.getStringExtra("searchTag")
if (searchTag != null) {
//tag按钮搜索
this.search.setText(searchTag)
searchAction(searchTag)
} else {
//初次启动
loadData( tags,1,ISADDDATA)
}
val close = findViewById<View>(R.id.close)
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_baseline_menu_24)
}
searchBar = findViewById(R.id.search_bar)
shortAnnotationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
close.setOnClickListener {
hiddenSearchBar()
hideIm()
}
search.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchAction(search.text.toString())
searchTag = search.text.toString()
hiddenSearchBar()
}
return@setOnEditorActionListener false
}
//刷新
swipeRefreshLayout.setOnRefreshListener {
nowPage = 1
loadData( searchTag, nowPage, ISREFRESH)
}
//侧边栏
val sp = getSharedPreferences("username", 0)
username = sp.getString("username", null)
val nav = findViewById<NavigationView>(R.id.nav)
//加载导航栏列表
nav.setCheckedItem(R.id.photo)
//设置侧边栏点击逻辑
nav.setNavigationItemSelectedListener {
when (it.itemId) {
//画廊
R.id.photo -> {
loadData( null, 1, ISREFRESH)
drawerLayout.closeDrawers()
searchTag = ""
true
}
//设置
R.id.setting -> {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
drawerLayout.closeDrawers()
false
}
R.id.taglist -> {
val intent = Intent(this, FavTagActivity::class.java)
startActivity(intent)
drawerLayout.closeDrawers()
false
}
else -> false
}
}
}
override fun onResume() {
super.onResume()
//加载源改变
val configSp = getSharedPreferences("com.lsper.view_preferences", 0)
if (nowSourceName != configSp.getString("sourceName",null)){
loadData(tags,1,ISREFRESH)
nowSourceName = configSp.getString("sourceName",null)
}
if (safeMode != configSp.getBoolean("safe_mode",true)){
loadData(tags,1,ISREFRESH)
safeMode = configSp.getBoolean("safe_mode",true)
}
}
//隐藏搜索栏
private fun hiddenSearchBar() {
searchBar.animate()
.alpha(0f)
.setDuration(shortAnnotationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
searchBar.visibility = View.GONE
}
})
hideIm()
barShow = false
}
//现实搜索栏
private fun showSearchBar() {
searchBar.apply {
alpha = 0f
visibility = View.VISIBLE
animate()
.alpha(1f)
.setDuration(shortAnnotationDuration.toLong())
.setListener(null)
}
barShow = true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
val drawerLayout = findViewById<DrawerLayout>(R.id.drawerLayout)
drawerLayout.openDrawer(GravityCompat.START)
}
R.id.search_nav -> {
if (barShow) {
searchTag = search.text.toString()
searchAction(searchTag)
hiddenSearchBar()
}
if (searchBar.visibility == View.GONE) {
showSearchBar()
}
}
}
return true
}
private fun hideIm() {
val controller = ViewCompat.getWindowInsetsController(window.decorView)
controller?.hide(ime())
}
//执行搜索
private fun searchAction(tags: String?) {
var tag = tags
var isNum = true
try {
tags?.toInt()
}catch (e:NumberFormatException){
isNum = false
}
if (isNum){
tag = "id:"+tags
}
loadData(tag, 1, ISREFRESH)
}
//设置列表数据
private fun loadData(tags: String?,page: Int,type:Int){
//缓存搜索的tags
this.tags = tags
val swipeRefreshLayout =
findViewById<androidx.swiperefreshlayout.widget.SwipeRefreshLayout>(
R.id.swipeRefreshLayout
)
swipeRefreshLayout.isRefreshing = true
//读取配置
val configSp = getSharedPreferences("com.lsper.view_preferences", 0)
val nowSourceName: String? = configSp.getString("sourceName",null)
var source = ""
for ((index,sourceName) in sourceNameArray.withIndex()){
if (sourceName == nowSourceName){
source = sourceUrlArray[index]
}
}
//接收异步信息
val handler = object : Handler(Looper.myLooper()!!){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when (msg.what){
CallBackStatus.DATAISNULL.ordinal ->{
Snackbar.make(swipeRefreshLayout,R.string.toast_load_empty,Snackbar.LENGTH_SHORT).show()
search.setText("")
}
CallBackStatus.OK.ordinal -> {
if (type == ISREFRESH )
//刷新数据
adapter.refreshData(msg.obj as ArrayList<Post_yand>)
else if (type == ISADDDATA)
//加载数据
adapter.addData(msg.obj as ArrayList<Post_yand>)
}
CallBackStatus.NETWORKERROR.ordinal -> {
Snackbar.make(swipeRefreshLayout,R.string.toast_load_network_fail,Snackbar.LENGTH_SHORT).show()
}
}
swipeRefreshLayout.isRefreshing = false
}
}
MainActivityModelImpl().requestPostList(handler,source,tags,page,configSp.getBoolean("safe_mode",true))
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar_menu, menu)
return true
}
}
@@ -1,139 +0,0 @@
package com.lsp.view.activity.main
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.lsp.view.R
import com.lsp.view.activity.pic.PicActivity
import com.lsp.view.bean.Post_yand
class PostAdapter(val context: Context, private var postYandList: ArrayList<Post_yand>) :
RecyclerView.Adapter<PostAdapter.ViewHolder>() {
val TAG = this::class.java.simpleName
private val UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val picImage: ImageView = view.findViewById(R.id.picImgae)
}
fun addData(list: ArrayList<Post_yand>) {
val pos = postYandList.size
postYandList.addAll(list)
notifyItemRangeInserted(pos, list.size)
}
fun refreshData(list: ArrayList<Post_yand>) {
val oldSize = postYandList.size
postYandList.clear()
notifyItemRangeRemoved(0,oldSize)
postYandList.addAll(list)
notifyItemRangeInserted(0, list.size)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.img_item_layout, parent, false)
val viewHolder = ViewHolder(view)
viewHolder.picImage.setOnClickListener {
val position = viewHolder.adapterPosition
Log.w("position", position.toString())
Log.w("url", postYandList[position].sample_url)
Log.w("rating", postYandList[position].rating)
var file_ext = postYandList[position].file_ext
if (file_ext == null){
val strarr = postYandList[position].sample_url.split(".")
file_ext = strarr[strarr.lastIndex]
}
PicActivity.actionStartActivity(context,postYandList[position].id,postYandList[position].sample_url,
postYandList[position].file_url,postYandList[position].tags,file_ext,
postYandList[position].author,postYandList[position].file_size,postYandList[position].md5)
}
return viewHolder
}
private lateinit var mLoadMoreListener: OnLoadMoreListener
interface OnLoadMoreListener {
fun loadMore(position: Int)
}
fun setLoadMoreListener(mLoadMoreListener: OnLoadMoreListener) {
this.mLoadMoreListener = mLoadMoreListener
}
private fun preLoad(p:Int){
if (p%20==0){
//执行预加载
val last:Int = if (postYandList.size-p<20){
postYandList.size-1
}else{
p+19
}
for(index in p..last){
if (p>6) {
val source: String = postYandList[index].sample_url
val glideUrl = GlideUrl(
source,
LazyHeaders.Builder().addHeader("User-Agent", UA)
.build()
)
Glide.with(context).download(glideUrl).preload()
}
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
preLoad(position)
val post = postYandList[position]
val source: String = post.sample_url
val height = postYandList[position].sample_height
val width = postYandList[position].sample_width
//图片过长则缩减高度以保证显示完整
if (width*0.75 > height){
holder.picImage.layoutParams.height = height / 2
}else {
holder.picImage.layoutParams.height = height
}
val glideUrl = GlideUrl(
source,
LazyHeaders.Builder().addHeader("User-Agent", UA)
.build()
)
Glide.with(context).load(glideUrl).into(holder.picImage)
if (position == postYandList.size - 1 && postYandList.size > 6) {
//到达底部
mLoadMoreListener.loadMore(position)
}
}
override fun getItemCount(): Int {
return postYandList.size
}
}
@@ -1,7 +0,0 @@
package com.lsp.view.activity.model
import android.os.Handler
interface MainActivityModel {
fun requestPostList(handler: Handler, source: String, tage: String?, page: Int, safeMode: Boolean)
}
@@ -1,70 +0,0 @@
package com.lsp.view.activity.model
import android.os.Handler
import android.os.Message
import com.lsp.view.retrofit.PostService
import com.lsp.view.retrofit.ServiceCreator
import com.lsp.view.bean.Post
import com.lsp.view.bean.Post_yand
import com.lsp.view.util.CallBackStatus
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.collections.ArrayList
class MainActivityModelImpl :MainActivityModel {
private val TAG = this::class.java.simpleName
override fun requestPostList(handler: Handler, source: String, tage: String?, page: Int,safeMode: Boolean){
val postService: PostService = ServiceCreator.create(source)
val service: Call<ArrayList<Post_yand>> = postService.getPostData("100",tage, page)
request(service,safeMode, handler)
}
fun <T:Post> callBack(handler: Handler,status: CallBackStatus,value:ArrayList<T>){
val msg = Message.obtain()
msg.what = status.ordinal
msg.obj = value
handler.sendMessage(msg)
}
fun callBack(handler: Handler,status: CallBackStatus){
val msg = Message.obtain()
msg.what = status.ordinal
handler.sendMessage(msg)
}
/**
* @param service ArrayList接收边界为Post的泛型,若直接使用Post作为泛型参数,需要强制类型转换。
*/
fun <T : Post> request(service:Call<ArrayList<T>>,safeMode: Boolean,handler: Handler){
service.enqueue(object : Callback<ArrayList<T>>{
override fun onResponse(call: Call<ArrayList<T>>, response: Response<ArrayList<T>>) {
val getValue:ArrayList<T>? = response.body()
if (getValue!=null) {
if (getValue.size==0){
callBack(handler,CallBackStatus.DATAISNULL)
return
}
if (safeMode) {
val iter = getValue.iterator()
while (iter.hasNext()) {
val post = iter.next()
if (post.rating != "s")
iter.remove()
}
}
callBack(handler,CallBackStatus.OK,getValue)
}
}
override fun onFailure(call: Call<ArrayList<T>>, t: Throwable) {
callBack(handler,CallBackStatus.NETWORKERROR)
}
})
}
}
@@ -1,33 +0,0 @@
package com.lsp.view.activity.pic
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.lsp.view.R
import com.lsp.view.bean.Author
class AuthorAdapter(val tagList: List<Author>) : RecyclerView.Adapter<AuthorAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val tagText = view.findViewById<TextView>(R.id.tag)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.tag_item_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val tag = tagList[position]
holder.tagText.text = tag.author
if (position == 0) {
holder.tagText.setBackgroundResource(R.drawable.title_bg)
}
}
override fun getItemCount(): Int {
return tagList.size
}
}
@@ -1,65 +0,0 @@
package com.lsp.view.activity.pic
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.lsp.view.R
import com.lsp.view.bean.ID
import com.lsp.view.bean.Tags
class IdAdapter(val idList: List<ID>,val context: Context) : RecyclerView.Adapter<IdAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val tagText = view.findViewById<TextView>(R.id.tag)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.tag_item_layout, parent, false)
val viewHolder = ViewHolder(view)
viewHolder.tagText.setOnLongClickListener {
val position = viewHolder.adapterPosition
if (position == 0) {
//标题
return@setOnLongClickListener false
}
var tagsArray: ArrayList<Tags> = ArrayList()
val tagsArraySp = context.getSharedPreferences("tags_sp", Context.MODE_PRIVATE)
val tagsArrayJson = tagsArraySp.getString("array", null)
if (tagsArrayJson != null) {
val tagListType = object : TypeToken<ArrayList<Tags>>() {}.type
tagsArray = Gson().fromJson(tagsArrayJson, tagListType)
}
for (tag: Tags in tagsArray) {
if (tag.tag == viewHolder.tagText.text.toString()) {
Snackbar.make(viewHolder.tagText, "收藏过了哦", Snackbar.LENGTH_SHORT).show()
return@setOnLongClickListener true
}
}
tagsArray.add(Tags(viewHolder.tagText.text.toString()))
tagsArraySp.edit().putString("array", Gson().toJson(tagsArray)).apply()
Snackbar.make(viewHolder.tagText, "收藏了新标签", Snackbar.LENGTH_SHORT).show()
return@setOnLongClickListener true
}
return viewHolder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val tag = idList[position]
holder.tagText.text = tag.id
if (position == 0) {
holder.tagText.setBackgroundResource(R.drawable.title_bg)
}
}
override fun getItemCount(): Int {
return idList.size
}
}
@@ -1,316 +0,0 @@
package com.lsp.view.activity.pic
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.*
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.createBitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.chrisbanes.photoview.PhotoView
import com.google.android.flexbox.*
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.lsp.view.R
import com.lsp.view.activity.BaseActivity
import com.lsp.view.activity.main.MainActivity
import com.lsp.view.bean.Author
import com.lsp.view.bean.ID
import com.lsp.view.bean.Size
import com.lsp.view.bean.Tags
import com.lsp.view.util.Util
import java.io.File
import kotlin.properties.Delegates
class PicActivity : BaseActivity() {
private val tagList = ArrayList<Tags>()
private val authorList = ArrayList<Author>()
private val idList = ArrayList<ID>()
private val sizeList = ArrayList<Size>()
private lateinit var image: ImageView
private lateinit var photoView: PhotoView
private var shortAnnotationDuration by Delegates.notNull<Int>()
private var md5 : String?= null
companion object{
fun actionStartActivity(context: Context,id:String,sample_url:String,file_url:String,tags:String,
file_ext:String,author:String,file_size:String,md5:String){
val intent=Intent(context,PicActivity::class.java)
intent.putExtra("id", id)
intent.putExtra("sample_url", sample_url)
intent.putExtra("file_url", file_url)
intent.putExtra("tags", tags)
intent.putExtra("file_ext", file_ext)
intent.putExtra("author", author)
intent.putExtra("file_size", file_size)
intent.putExtra("md5", md5)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pic)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
image = findViewById(R.id.titleImage)
shortAnnotationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
val intent = intent
val tags = intent.getStringExtra("tags")
if (tags != null) {
loadTags(tags, "tags")
}
val author = intent.getStringExtra("author")
if (author != null) {
loadTags(author, "author")
}
val file_size = intent.getStringExtra("file_size")
if (file_size != null) {
loadTags(file_size, "size")
}
md5 = intent.getStringExtra("md5")
val sharedPreferences = getSharedPreferences("FirstRun", 0)
val firstRun = sharedPreferences.getBoolean("FirstRun", true)
if (firstRun) {
val sharedPreferences = getSharedPreferences("FirstRun", 0).edit()
sharedPreferences.putBoolean("FirstRun", false).apply()
val FileD =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake")
FileD.mkdirs()
}
val id = intent.getStringExtra("id")
if (id != null) {
loadTags(id, "id")
}
val sample_url = intent.getStringExtra("sample_url")
sample_url?.let {
loadPic(it)
}
val file_url = intent.getStringExtra("file_url")
val file_ext = intent.getStringExtra("file_ext")
val download =
findViewById<FloatingActionButton>(
R.id.download
)
photoView = findViewById(R.id.photoView)
image.setOnClickListener {
photoView.apply {
alpha = 0f
visibility = View.VISIBLE
animate()
.alpha(1f)
.setDuration(shortAnnotationDuration.toLong())
.setListener(null)
}
Glide.with(this).load(sample_url).into(photoView)
}
photoView.setOnClickListener {
photoView.animate()
.alpha(0f)
.setDuration(shortAnnotationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
photoView.visibility = View.GONE
}
})
}
val share = findViewById<FloatingActionButton>(R.id.share)
share.setOnClickListener {
if (sample_url != null) {
Util.share(sample_url,this)
}
}
download.setOnClickListener {
Util.download(file_url, file_ext, md5)
}
val f_btn = findViewById<FloatingActionButton>(R.id.float_btn)
val ctrl = findViewById<LinearLayout>(R.id.ctrl)
f_btn.setOnClickListener {
if (ctrl.visibility == View.VISIBLE)
ctrl.visibility = View.GONE
else
ctrl.visibility = View.VISIBLE
}
val back = findViewById<ImageView>(R.id.back)
back.setOnClickListener {
finish()
}
}
override fun onBackPressed() {
if (photoView.visibility == View.GONE) {
super.onBackPressed()
} else {
photoView.animate()
.alpha(0f)
.setDuration(shortAnnotationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
photoView.visibility = View.GONE
}
})
}
}
private fun loadPic(url: String) {
val glideUrl = GlideUrl(
url,
LazyHeaders.Builder().addHeader(
"User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36"
).build()
)
Glide.with(this).load(glideUrl).listener(object : RequestListener<Drawable?> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable?>?,
isFirstResource: Boolean
): Boolean {
if (e != null) {
e.printStackTrace()
}
val back = findViewById<ImageView>(R.id.back)
Snackbar.make(back, R.string.toast_load_fail, Snackbar.LENGTH_LONG).setAction(R.string.button_check) {
AlertDialog.Builder(this@PicActivity).apply {
setTitle("Log")
if (e != null) {
setMessage(e.stackTraceToString())
}
setNegativeButton(R.string.button_ok, null)
create()
show()
}
}.show()
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
return false
}
}).into(image)
}
private fun layoutManager(): FlexboxLayoutManager {
val manager = object : FlexboxLayoutManager(this, FlexDirection.ROW, FlexWrap.WRAP) {
override fun canScrollVertically(): Boolean {
return false
}
}
manager.alignItems = AlignItems.CENTER
manager.justifyContent = JustifyContent.FLEX_START
return manager
}
//设置tags列表
//这写的很烂 得改
private fun loadTags(tags: String, type: String) {
when (type) {
"tags" -> {
tagList.add(Tags("Tag"))
val list = tags.split(" ")
val tagRecyclerView =
findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.tagRecyclerView)
tagRecyclerView.layoutManager = layoutManager()
for (tag in list) run {
tagList.add(Tags(tag))
}
val adapter = TagAdapter(tagList, this)
adapter.setOnItemClickListener(object : TagAdapter.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
val intent = Intent(this@PicActivity, MainActivity::class.java)
intent.putExtra("searchTag", tagList[position].tag)
startActivity(intent)
}
})
tagRecyclerView.adapter = adapter
}
"author" -> {
authorList.add(Author("Author"))
val authorRecyclerView =
findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.authorRecyclerView)
authorRecyclerView.layoutManager = layoutManager()
authorList.add(Author(tags))
val adapter = AuthorAdapter(authorList)
authorRecyclerView.adapter = adapter
}
"id" -> {
idList.add(ID("ID"))
idList.add(ID(tags))
val idRecyclerView =
findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.idRecyclerView)
idRecyclerView.layoutManager = layoutManager()
val adapter = IdAdapter(idList,this)
idRecyclerView.adapter = adapter
}
"size" -> {
sizeList.add(Size("Size"))
sizeList.add(Size(tags))
val sizeRecyclerView =
findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.sizeRecyclerView)
sizeRecyclerView.layoutManager = layoutManager()
val adapter = SizeAdapter(sizeList)
sizeRecyclerView.adapter = adapter
}
}
}
}
@@ -1,41 +0,0 @@
package com.lsp.view.activity.pic
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.lsp.view.R
import com.lsp.view.bean.Size
import java.math.RoundingMode
import java.text.DecimalFormat
class SizeAdapter(val tagList: List<Size>) : RecyclerView.Adapter<SizeAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val tagText = view.findViewById<TextView>(R.id.tag)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.tag_item_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val tag = tagList[position]
if (position == 0) {
holder.tagText.text = tag.file_size
holder.tagText.setBackgroundResource(R.drawable.title_bg)
} else {
val sizeKb = tag.file_size.toFloat() / 1024
val format = DecimalFormat("0.##")
format.roundingMode = RoundingMode.FLOOR
val size = format.format(sizeKb)
holder.tagText.text = "$size KB"
}
}
override fun getItemCount(): Int {
return tagList.size
}
}
@@ -1,79 +0,0 @@
package com.lsp.view.activity.pic
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.lsp.view.R
import com.lsp.view.bean.Tags
class TagAdapter(val tagList: List<Tags>, val context: Context) :
RecyclerView.Adapter<TagAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val tagText = view.findViewById<TextView>(R.id.tag)
}
private lateinit var mOnItemClickListener: OnItemClickListener
interface OnItemClickListener {
fun onItemClick(view: View, position: Int)
}
fun setOnItemClickListener(mOnItemClickListener: OnItemClickListener) {
this.mOnItemClickListener = mOnItemClickListener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.tag_item_layout, parent, false)
val viewHolder = ViewHolder(view)
viewHolder.tagText.setOnLongClickListener {
val position = viewHolder.adapterPosition
if (position == 0) {
//标题
return@setOnLongClickListener false
}
var tagsArray: ArrayList<Tags> = ArrayList()
val tagsArraySp = context.getSharedPreferences("tags_sp", Context.MODE_PRIVATE)
val tagsArrayJson = tagsArraySp.getString("array", null)
if (tagsArrayJson != null) {
val tagListType = object : TypeToken<ArrayList<Tags>>() {}.type
tagsArray = Gson().fromJson(tagsArrayJson, tagListType)
}
for (tag: Tags in tagsArray) {
if (tag.tag == viewHolder.tagText.text.toString()) {
Snackbar.make(viewHolder.tagText, R.string.toast_tag_exist, Snackbar.LENGTH_SHORT).show()
return@setOnLongClickListener true
}
}
tagsArray.add(Tags(viewHolder.tagText.text.toString()))
tagsArraySp.edit().putString("array", Gson().toJson(tagsArray)).apply()
Snackbar.make(viewHolder.tagText, R.string.toast_tag_add_fav, Snackbar.LENGTH_SHORT).show()
return@setOnLongClickListener true
}
return viewHolder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val tag = tagList[position]
holder.tagText.text = tag.tag
holder.tagText.setOnClickListener {
mOnItemClickListener.onItemClick(it, position)
}
if (position == 0) {
holder.tagText.setBackgroundResource(R.drawable.title_bg)
}
}
override fun getItemCount(): Int {
return tagList.size
}
}
@@ -1,51 +0,0 @@
package com.lsp.view.activity.setting
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.appbar.AppBarLayout
import com.lsp.view.YandViewApplication
import com.lsp.view.R
import com.lsp.view.activity.BaseActivity
class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val appbar = findViewById<AppBarLayout>(R.id.appbar)
val nowHeight = appbar.layoutParams.height
appbar.layoutParams.height = (application as YandViewApplication).statusBarHeight()+nowHeight
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
finish()
}
}
return super.onOptionsItemSelected(item)
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
}
}
}
@@ -1,3 +0,0 @@
package com.lsp.view.bean
class Author(val author: String)
@@ -1,3 +0,0 @@
package com.lsp.view.bean
class ID(val id: String)
@@ -1,7 +0,0 @@
package com.lsp.view.bean
open class Post(
val id: String,
val rating: String,
val author: String,
)
@@ -1,16 +0,0 @@
package com.lsp.view.bean
class Post_yand(
val preview_url: String,
val file_url: String,
val sample_url: String,
val tags: String,
val file_ext: String,
val file_size: String,
val sample_height : Int,
val sample_width: Int,
id: String,
rating: String,
author: String,
val md5 : String
):Post(id, rating, author)
@@ -1,3 +0,0 @@
package com.lsp.view.bean
class Size(val file_size: String)
@@ -1,3 +0,0 @@
package com.lsp.view.bean
class Tags(val tag: String)
@@ -1,149 +0,0 @@
package com.lsp.view.service
import android.app.Service
import android.content.Context
import android.content.Intent
import android.media.MediaScannerConnection
import android.os.*
import com.lsp.view.util.CallBackStatus
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.security.MessageDigest
import kotlin.concurrent.thread
class DownloadService : Service() {
private val mBinder = DownloadBinder(this)
class DownloadBinder(val context: Context) : Binder() {
fun callBack(handler: Handler,status:CallBackStatus,obj:Any?=null){
val msg = Message.obtain()
msg.what = status.ordinal
if (obj!=null){
msg.obj = obj
}
handler.sendMessage(msg)
}
fun downloadPic(file_url: String, end: String,handler: Handler,md5 : String?) {
thread {
val FileD =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/")
if (FileD.exists()) {
val file =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/$md5.$end")
if (file.exists()){
callBack(handler,CallBackStatus.FILEEXISTS)
return@thread
}
val fos = FileOutputStream(file)
try {
val client = OkHttpClient()
val request = Request.Builder()
.url(file_url)
.build()
val response = client.newCall(request).execute()
val responseData = response.body()?.bytes()
if (response.code() == 200) {
fos.write(responseData)
//通知媒体更新
MediaScannerConnection.scanFile(
context, arrayOf(file.path),
null, null
)
if (md5 == getFileMD5(file.path)) {
callBack(handler, CallBackStatus.OK,file)
} else {
file.delete()
callBack(handler,CallBackStatus.MD5COMPAREERROR)
}
return@thread
} else {
file.delete()
callBack(handler, CallBackStatus.NETWORKERROR)
return@thread
}
} catch (e: Exception) {
e.printStackTrace()
callBack(handler, CallBackStatus.DOWNLOADERROR)
return@thread
} finally {
fos.close()
}
} else {
FileD.mkdirs()
downloadPic(file_url, end, handler, md5)
}
callBack(handler,CallBackStatus.DOWNLOADERROR)
}
}
/**
* 获取文件MD5
*/
fun getFileMD5(path: String?): String? {
if (path.isNullOrEmpty()) {
return null
}
var digest: MessageDigest? = null
var fileIS: FileInputStream? = null
val buffer = ByteArray(1024)
var len = 0
try {
digest = MessageDigest.getInstance("MD5")
val oldF = File(path)
fileIS = FileInputStream(oldF)
while (fileIS.read(buffer).also { len = it } != -1) {
digest.update(buffer, 0, len)
}
} catch (e: Exception) {
e.printStackTrace()
return null
}finally {
fileIS?.close()
}
return bytesToHexString(digest?.digest())
}
fun bytesToHexString(src: ByteArray?): String? {
val result = StringBuilder("")
if (src?.isEmpty()==true) {
return null
}
src?.forEach {
var i = it.toInt()
//这里需要对b与0xff做位与运算,
//若b为负数,强制转换将高位位扩展,导致错误,
//故需要高位清零
val hexStr = Integer.toHexString(i and 0xff)
//若转换后的十六进制数字只有一位,
//则在前补"0"
if (hexStr.length == 1) {
result.append(0)
}
result.append(hexStr)
}
return result.toString()
}
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
}
@@ -1,5 +0,0 @@
package com.lsp.view.util
enum class CallBackStatus {
OK, NETWORKERROR, DATAISNULL, DOWNLOADERROR, SETWP, MD5COMPAREERROR, FILEEXISTS
}
-104
View File
@@ -1,104 +0,0 @@
package com.lsp.view.util
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.widget.Toast
import androidx.core.content.FileProvider
import com.bumptech.glide.Glide
import com.lsp.view.R
import com.lsp.view.YandViewApplication
import java.io.File
import kotlin.concurrent.thread
object Util {
fun share(url:String,context: Context){
thread {
val bitmap = Glide.with(context).asBitmap().load(url).submit().get()
val path = File("${context.cacheDir}/image")
if (!path.exists())
path.mkdirs()
val file = File("${context.cacheDir}/image/cache.png")
if (file.exists()){
file.delete()
}
file.outputStream().apply {
bitmap.compress(Bitmap.CompressFormat.PNG,100,this)
}
val imageUri = FileProvider.getUriForFile(
context,
"com.lsper.view.fileprovider",
file
)
context.grantUriPermission("com.lsper.view",imageUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "image/*"
shareIntent.putExtra(Intent.EXTRA_STREAM,imageUri)
val intent = Intent.createChooser(shareIntent,R.string.title_share.toString())
val resInfoList: List<ResolveInfo> = context.packageManager
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
context.grantUriPermission(
packageName,
imageUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
context.startActivity(intent)
}
}
fun download(file_url: String?, file_ext: String?,md5: String?){
Toast.makeText(YandViewApplication.context, R.string.toast_download_start, Toast.LENGTH_SHORT).show()
if (file_url != null) {
if (file_ext != null) {
YandViewApplication.downloadBinder?.downloadPic(file_url, file_ext,handler,md5)
}
}
}
private val handler = object : Handler(Looper.myLooper()!!){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when(msg.what){
CallBackStatus.OK.ordinal ->{
Toast.makeText(YandViewApplication.context, R.string.toast_download_success, Toast.LENGTH_SHORT).show()
}
CallBackStatus.DOWNLOADERROR.ordinal -> {
Toast.makeText(YandViewApplication.context, R.string.toast_download_fail, Toast.LENGTH_SHORT).show()
}
CallBackStatus.MD5COMPAREERROR.ordinal -> {
Toast.makeText(YandViewApplication.context, R.string.toast_compar_md5_fail, Toast.LENGTH_SHORT).show()
}
CallBackStatus.FILEEXISTS.ordinal -> {
Toast.makeText(YandViewApplication.context, R.string.toast_file_exist, Toast.LENGTH_SHORT).show()
}
}
}
}
}
@@ -0,0 +1,21 @@
package moe.uni.view
import android.app.Application
import androidx.lifecycle.DefaultLifecycleObserver
import android.content.Context
class YandViewApplication : Application(), DefaultLifecycleObserver {
override fun onCreate() {
super<Application>.onCreate()
context = applicationContext
}
companion object {
var context: Context? = null
private set
const val UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"
}
}
@@ -0,0 +1,53 @@
package moe.uni.view.bean
import android.os.Parcel
import android.os.Parcelable
import java.io.Serializable
open class Post(
open val postId: String,
open val rating: String,
open val sampleUrl:String,
open val fileUrl:String,
open val sampleHeight: Int,
open val sampleWidth: Int,
open val tags:String?
): Parcelable,Serializable {
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readInt(),
parcel.readInt(),
parcel.readString()!!
)
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(postId)
dest.writeString(rating)
dest.writeString(sampleUrl)
dest.writeString(fileUrl)
dest.writeInt(sampleHeight)
dest.writeInt(sampleWidth)
dest.writeString(tags)
}
companion object {
@JvmField
val CREATOR = object:Parcelable.Creator<Post> {
override fun createFromParcel(parcel: Parcel): Post {
return Post(parcel)
}
override fun newArray(size: Int): Array<Post?> {
return arrayOfNulls(size)
}
}
}
}
@@ -0,0 +1,13 @@
package moe.uni.view.bean
import com.google.gson.annotations.SerializedName
class YandPost(
@SerializedName("file_url") override val fileUrl: String,
@SerializedName("sample_url") override val sampleUrl: String,
@SerializedName("sample_height") override val sampleHeight : Int,
@SerializedName("sample_width") override val sampleWidth: Int,
@SerializedName("id") override val postId: String,
rating: String,
tags:String?
):Post(postId, rating, sampleUrl,fileUrl,sampleHeight,sampleWidth,tags)
@@ -0,0 +1,27 @@
package moe.uni.view.common
class Config(val safeMode:Boolean,val source:String) {
companion object{
fun setConfig(config: Config){
PreUtils.putString(PreKV.SOURCE_NAME,config.source)
PreUtils.putBool(PreKV.SAFE_MODE,config.safeMode)
}
fun getConfig():Config{
return Config(PreUtils.getBool(PreKV.SAFE_MODE,true),PreUtils.getString(PreKV.SOURCE_NAME,SOURCE.YANDE_RE.values.first()))
}
fun setSafeMode(value:Boolean){
PreUtils.putBool(PreKV.SAFE_MODE,value)
}
fun getSafeMode():Boolean{
return PreUtils.getBool(PreKV.SAFE_MODE,true)
}
}
}
object SOURCE {
val YANDE_RE = mapOf("yande.re" to "https://yande.re/")
val KONACHAN = mapOf("konachan" to "https://konachan.com/")
}
@@ -0,0 +1,102 @@
package moe.uni.view.common
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.request.ImageRequest
import moe.uni.view.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
data class ImageInfo(val path: String, val name: String, val uri: Uri)
@SuppressLint("QueryPermissionsNeeded")
fun share(url: String, context: Context) {
CoroutineScope(Dispatchers.IO).launch {
val imageInfo = getImageInfo(url, context)
context.grantUriPermission(
context.packageName,
imageInfo.uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "image/*"
shareIntent.putExtra(Intent.EXTRA_STREAM, imageInfo.uri)
shareIntent.putExtra(Intent.EXTRA_TITLE, imageInfo.name)
val intent = Intent.createChooser(shareIntent, R.string.title_share.toString())
val resInfoList: List<ResolveInfo> = if (Build.VERSION.SDK_INT < 33) {
context.packageManager
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
} else {
context.packageManager.queryIntentActivities(
intent,
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
)
}
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
context.grantUriPermission(
packageName,
imageInfo.uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
context.startActivity(intent)
}
}
fun setWallpaper(url: String,context: Context) {
CoroutineScope(Dispatchers.IO).launch {
val imageInfo = getImageInfo(url, context)
val intent = Intent(Intent.ACTION_ATTACH_DATA)
intent.setDataAndType(imageInfo.uri, "image/*")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
}
}
suspend fun getImageInfo(url: String, context: Context): ImageInfo {
val imageLoader = ImageLoader.Builder(context)
.build()
val imageRequest = ImageRequest.Builder(context).data(url).allowHardware(true).build()
val cacheImage = imageLoader.execute(imageRequest)
val bitmap = cacheImage.drawable?.toBitmap()
val path = File("${context.cacheDir}/image")
val split = url.split("/")
val fileName = split[split.size - 1]
if (!path.exists())
path.mkdirs()
var file = File("${context.cacheDir}/image/$fileName")
val downloadFile =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/$fileName")
if (downloadFile.exists()) {
file = downloadFile
} else if (!file.exists()) {
file.outputStream().apply {
bitmap?.compress(Bitmap.CompressFormat.PNG, 100, this)
}
}
val imageUri = FileProvider.getUriForFile(
context,
"moe.uni.view.fileprovider",
file
)
return ImageInfo(path = file.path, name = fileName, uri = imageUri)
}
@@ -0,0 +1,10 @@
package moe.uni.view.common
object PreKV {
const val SOURCE_NAME = "source_name"
const val SAFE_MODE = "safe_mode"
}
object Pre{
const val NAME = "moe.uni.view_preferences"
}
@@ -0,0 +1,23 @@
package moe.uni.view.common
import android.content.Context
import moe.uni.view.YandViewApplication
object PreUtils {
private val PRE = YandViewApplication.context!!.getSharedPreferences(Pre.NAME,Context.MODE_PRIVATE)
fun getString(key:String,default:String):String{
return PRE.getString(key,default)!!
}
fun putString(key: String,value:String){
PRE.edit().putString(key,value).apply()
}
fun getBool(key: String,default:Boolean):Boolean{
return PRE.getBoolean(key,default)
}
fun putBool(key: String,value: Boolean){
PRE.edit().putBoolean(key, value).apply()
}
}
@@ -0,0 +1,24 @@
package moe.uni.view.repository.datasource
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import moe.uni.view.repository.datasource.dao.CollectDao
import moe.uni.view.repository.datasource.model.Collect
@Database(entities = [Collect::class], version = 1,exportSchema = false)
abstract class CollectDatabase: RoomDatabase(){
abstract fun collectDao():CollectDao
companion object{
@Volatile
private var Instance:CollectDatabase? = null
fun getDatabase(context: Context):CollectDatabase{
return Instance?: synchronized(this) {
Room.databaseBuilder(context,CollectDatabase::class.java,"collect_database").build().also { Instance = it }
}
}
}
}
@@ -0,0 +1,12 @@
package moe.uni.view.repository.datasource
import moe.uni.view.repository.datasource.model.Collect
interface CollectRepository {
suspend fun addNewCollect(collect: Collect)
suspend fun getAllCollect():List<Collect>
suspend fun getCollectByPostId(postId:String,type:String):Collect?
suspend fun removeCollect(collect: Collect)
}
@@ -0,0 +1,23 @@
package moe.uni.view.repository.datasource
import moe.uni.view.repository.datasource.dao.CollectDao
import moe.uni.view.repository.datasource.model.Collect
class CollectRepositoryImpl(private val collectDao:CollectDao):CollectRepository {
override suspend fun addNewCollect(collect: Collect) {
collectDao.insert(collect)
}
override suspend fun getAllCollect(): List<Collect> {
return collectDao.getAllCollect()
}
override suspend fun getCollectByPostId(postId: String,type:String): Collect? {
return collectDao.getCollectByPostId(postId,type)
}
override suspend fun removeCollect(collect: Collect) {
collectDao.delete(collect.postId,collect.type)
}
}
@@ -0,0 +1,19 @@
package moe.uni.view.repository.datasource.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import moe.uni.view.repository.datasource.model.Collect
@Dao
interface CollectDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(collect:Collect)
@Query("select * from collect")
suspend fun getAllCollect():List<Collect>
@Query("delete from collect where post_id = :postId and type = :type")
suspend fun delete(postId: String,type: String)
@Query("select * from collect where post_id = :postId and type = :type")
suspend fun getCollectByPostId(postId:String,type:String):Collect?
}
@@ -0,0 +1,37 @@
package moe.uni.view.repository.datasource.model
import android.content.Context
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import moe.uni.view.YandViewApplication
import moe.uni.view.bean.Post
@Entity(tableName = "collect")
data class Collect(
@PrimaryKey(autoGenerate = true) var id:Int = 0,
@ColumnInfo(name = "post_id")override val postId: String,
@ColumnInfo(name = "sample_url")override val sampleUrl:String,
@ColumnInfo(name = "rating")override val rating:String,
@ColumnInfo(name = "file_url")override val fileUrl:String,
@ColumnInfo(name = "sampleHeight") override val sampleHeight:Int,
@ColumnInfo(name = "sampleWeight") override val sampleWidth:Int,
@ColumnInfo(name = "type")val type:String,
@ColumnInfo(name = "tags") override val tags:String?
):Post(postId,rating,sampleUrl,fileUrl,sampleHeight,sampleWidth,tags){
constructor(
postId: String,
sampleUrl:String,
rating:String,
fileUrl:String,
sampleHeight:Int,
sampleWeight:Int,
tags:String?
): this(0, postId, sampleUrl, rating, fileUrl, sampleHeight, sampleWeight,
YandViewApplication.context?.getSharedPreferences("moe.uni.view_preferences",Context.MODE_PRIVATE)?.getString("source_name", "yande.re")!!,tags)
companion object{
fun Post.toCollect():Collect = Collect(this.postId,this.sampleUrl,this.rating,this.fileUrl,this.sampleHeight,this.sampleWidth ,this.tags)
}
}
@@ -0,0 +1,13 @@
package moe.uni.view.repository.exception
import android.util.Log
open class LoggerException(override val message: String?):Exception() {
init {
logMessage()
}
private fun logMessage(){
Log.e(this::class.java.simpleName,message.toString())
}
}
@@ -0,0 +1,4 @@
package moe.uni.view.repository.exception
class NetworkErrorException(override val message: String?) : LoggerException(message) {
}
@@ -0,0 +1,4 @@
package moe.uni.view.repository.exception
class UnableConstructObjectException(override val message: String?):LoggerException(message) {
}
@@ -0,0 +1,35 @@
package moe.uni.view.repository.network
import moe.uni.view.repository.network.api.PostApi
import moe.uni.view.repository.network.api.PostApiImpl
import moe.uni.view.bean.YandPost
import moe.uni.view.common.Config
class PostDataSource {
companion object{
const val YANDE_RE = "yande.re"
const val KONACHAN = "konachan"
}
private val api: PostApi = PostApiImpl()
//设置列表数据
suspend fun fetchNewPost(load: Load): ArrayList<YandPost> {
return api.fetchNewPost(load)
}
}
data class Load(
val tags: String?,
val page: Int,
val safe:Boolean,
val source:String
){
companion object{
fun builder(tags: String?, page: Int): Load {
val config = Config.getConfig()
return Load(tags,page,config.safeMode,config.source)
}
}
}
@@ -0,0 +1,38 @@
package moe.uni.view.repository.network
import moe.uni.view.bean.YandPost
import moe.uni.view.common.Config
class PostRepository {
private val dataSource = PostDataSource()
//获取post
suspend fun fetchPostData(searchTarget :String?,page:Int): ArrayList<YandPost> {
var target = searchTarget
var isNum = true
//如果检索目标是纯数字,则按照id搜索
try {
target?.toLong()
}catch (e:NumberFormatException){
isNum = false
}
if (isNum){
target = "id:$searchTarget"
}
val load = Load.builder(target,page)
val dataList = dataSource.fetchNewPost(load)
return if (Config.getSafeMode()){
ArrayList(dataList.filter { it.rating == "s" })
}else{
dataList
}
}
}
@@ -0,0 +1,39 @@
package moe.uni.view.repository.network.api
import moe.uni.view.repository.network.Load
import moe.uni.view.bean.Post
import moe.uni.view.bean.YandPost
import moe.uni.view.repository.exception.NetworkErrorException
import retrofit2.Call
interface PostApi {
private val TAG: String
get() = this::class.java.simpleName
/**
* @param service ArrayList接收边界为Post的泛型,若直接使用Post作为泛型参数,需要强制类型转换。
*/
@Throws(NetworkErrorException::class)
fun <T : Post> request(service: Call<ArrayList<T>>, safeMode: Boolean):ArrayList<T> {
try {
val execute = service.execute()
return if (execute.isSuccessful) {
if (execute.body() != null) {
execute.body()!!
} else {
//结果为空,返回空集避免空指针
ArrayList()
}
} else {
throw NetworkErrorException(execute.errorBody().toString())
}
}catch (e: Exception){
throw NetworkErrorException(e.message)
}
}
suspend fun fetchNewPost(load: Load):ArrayList<YandPost>
}
@@ -0,0 +1,20 @@
package moe.uni.view.repository.network.api
import moe.uni.view.repository.network.Load
import moe.uni.view.repository.network.retrofit.PostService
import moe.uni.view.repository.network.retrofit.ServiceCreator
import moe.uni.view.bean.YandPost
import retrofit2.Call
import kotlin.collections.ArrayList
class PostApiImpl :PostApi {
private val TAG = this::class.java.simpleName
private val defaultLimit = "100"
override suspend fun fetchNewPost(load: Load):ArrayList<YandPost>{
val postService: PostService = ServiceCreator.create(load.source)
val service: Call<ArrayList<YandPost>> = postService.getPostData(defaultLimit,load.tags, load.page)
return request(service, load.safe)
}
}
@@ -1,6 +1,6 @@
package com.lsp.view.retrofit
package moe.uni.view.repository.network.retrofit
import com.lsp.view.bean.Post_yand
import moe.uni.view.bean.YandPost
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
@@ -11,5 +11,5 @@ interface PostService {
@Query("limit") limit: String,
@Query("tags") tags: String?,
@Query("page") page: Int
): Call<ArrayList<Post_yand>>
): Call<ArrayList<YandPost>>
}
@@ -1,23 +1,29 @@
package com.lsp.view.retrofit
package moe.uni.view.repository.network.retrofit
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ServiceCreator {
val CLIENT = OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).build()
/**
* @param serviceClass:所属类的Service接口
* @param source:加载根地址
*
*/
var source: String? = null
fun <T> create(serviceClass: Class<T>, source: String): T {
val retrofit = Retrofit.Builder()
.baseUrl(source)
.client(CLIENT)
.addConverterFactory(GsonConverterFactory.create())
.build()
ServiceCreator.source = source
return retrofit.create(serviceClass)
}
@@ -0,0 +1,88 @@
package moe.uni.view.service
import android.app.Service
import android.content.Context
import android.content.Intent
import android.media.MediaScannerConnection
import android.os.*
import moe.uni.view.repository.network.retrofit.ServiceCreator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
class Result(val success:Boolean,val message: String,val exception: Exception?){
companion object{
fun success(message:String):Result{
return Result(true,message,null)
}
fun failure(message: String = "",exception: Exception? = null):Result{
return Result(false,message,exception)
}
}
}
class DownloadService : Service() {
private val mBinder = DownloadBinder(this)
class DownloadBinder(val context: Context) : Binder() {
fun downloadImage(fileUrl: String): Deferred<Result> {
return CoroutineScope(Dispatchers.IO).async {
val fileDir =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/")
if (!fileDir.exists()) {
fileDir.mkdirs()
}
val split = fileUrl.split("/")
val fileName = split[split.size - 1]
val file =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/$fileName")
if (file.exists()) {
return@async Result.failure("File exists")
}
val fos = FileOutputStream(file)
try {
val request = Request.Builder()
.url(fileUrl)
.build()
val response = ServiceCreator.CLIENT.newCall(request).execute()
val responseData = response.body?.bytes()
return@async if (response.code == 200) {
fos.write(responseData)
//通知媒体更新
MediaScannerConnection.scanFile(
context, arrayOf(file.path),
null, null
)
Result.success("File download successfully")
} else {
file.delete()
Result.failure("Connection timed out")
}
} catch (e: Exception) {
Result.failure(exception = e)
} finally {
fos.close()
}
return@async Result.failure(message = "Unknown error")
}
}
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
}
@@ -0,0 +1,96 @@
package moe.uni.view.ui.compose
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import moe.uni.view.ui.compose.theme.LspViewTheme
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import moe.uni.view.service.DownloadService
import moe.uni.view.ui.compose.screen.DetailScreen
import moe.uni.view.ui.compose.screen.PostListScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private const val TAG = "Compose_PostActivity"
class PostActivity : ComponentActivity() {
lateinit var downloadBinder: DownloadService.DownloadBinder
val viewModel: PostViewModel by viewModels<PostViewModel>()
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
downloadBinder = iBinder as DownloadService.DownloadBinder
}
override fun onServiceDisconnected(componentName: ComponentName) {}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LspViewTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
) {
App()
}
}
}
viewModel.downloadAction.observe(this) {
CoroutineScope(Dispatchers.IO).launch {
val result =
downloadBinder.downloadImage(viewModel.uiState.value.selectPost?.fileUrl ?: "")
.await()
viewModel.setDownloadResult(result)
}
}
viewModel.downloadResult.observe(this){
val message = it.message
Toast.makeText(this,message,Toast.LENGTH_SHORT).show()
}
val serviceIntent = Intent(this, DownloadService::class.java)
bindService(serviceIntent, connection, BIND_AUTO_CREATE)
}
override fun onDestroy() {
super.onDestroy()
unbindService(connection)
}
}
const val NAV_ROUTE_POSTLIST_SCREEN = "NAV_ROUTE_POSTLIST_SCREEN"
const val NAV_ROUTE_DETAIL_SCREEN = "NAV_ROUTE_DETAIL_SCREEN"
@Composable
fun App(viewModel: PostViewModel = viewModel()) {
val navController = rememberNavController()
NavHost(navController, startDestination = NAV_ROUTE_POSTLIST_SCREEN) {
composable(NAV_ROUTE_POSTLIST_SCREEN) {
PostListScreen(navController = navController, viewModel = viewModel)
}
composable(NAV_ROUTE_DETAIL_SCREEN) {
DetailScreen(navController = navController, viewModel = viewModel)
}
}
}
@@ -0,0 +1,12 @@
package moe.uni.view.ui.compose
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import moe.uni.view.bean.Post
data class PostUiState(
var refresh:MutableState<Boolean> = mutableStateOf(true),
var searchTarget: MutableState<String> = mutableStateOf(""),
var page:Int = 1,
var selectPost: Post? = null
)
@@ -0,0 +1,67 @@
package moe.uni.view.ui.compose
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import moe.uni.view.bean.YandPost
import moe.uni.view.repository.exception.NetworkErrorException
import moe.uni.view.repository.network.PostRepository
import moe.uni.view.service.Result
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class PostViewModel:ViewModel() {
private val _uiState = MutableStateFlow(PostUiState())
val uiState = _uiState.asStateFlow()
val postData:MutableStateFlow<List<YandPost>> = MutableStateFlow(arrayListOf())
private val repository by lazy {
PostRepository()
}
val downloadAction = MutableLiveData<Unit>()
val downloadResult = MutableLiveData<Result>()
init {
initData()
}
private fun initData(){
viewModelScope.launch(Dispatchers.IO) {
fetchPost()
}
}
fun actionDownload(){
downloadAction.postValue(Unit)
}
suspend fun fetchPost(searchTarget: String = _uiState.value.searchTarget.value,refresh:Boolean = false){
_uiState.value.refresh.value = true
_uiState.value.searchTarget.value = searchTarget
try {
if (refresh){
_uiState.value.page = 0
}
val data = repository.fetchPostData(
searchTarget,
_uiState.value.page
)
postData.value = if (refresh){
data
}else{
_uiState.value.page ++
postData.value + data
}
} catch (e: NetworkErrorException) {
//todo network error
}
_uiState.value.refresh.value = false
}
fun setDownloadResult(result: Result){
downloadResult.postValue(result)
}
}
@@ -0,0 +1,121 @@
package moe.uni.view.ui.compose.screen
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
import coil.compose.AsyncImage
import moe.uni.view.R
import moe.uni.view.common.setWallpaper
import moe.uni.view.common.share
import moe.uni.view.ui.compose.PostViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailScreen(navController: NavController, viewModel: PostViewModel) {
val uiState by viewModel.uiState.collectAsState()
var scale by remember { mutableFloatStateOf(1f) }
var rotation by remember { mutableFloatStateOf(0f) }
val state = rememberTransformableState { zoomChange, _, rotationChange ->
scale *= zoomChange
rotation += rotationChange
}
val context = LocalContext.current
Box(
modifier = Modifier
.background(Color.Black)
.fillMaxSize()
) {
AsyncImage(
model = uiState.selectPost?.sampleUrl,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
)
.transformable(state = state)
.background(Color.Black)
)
Column {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
titleContentColor = MaterialTheme.colorScheme.primary,
actionIconContentColor = Color.White,
navigationIconContentColor = Color.White
),
title = {},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = R.string.description_back.toString())
}
},
actions = {
IconButton(onClick = {
viewModel.actionDownload()
Toast.makeText(context, R.string.toast_download_start, Toast.LENGTH_SHORT).show()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_twotone_arrow_downward_24),
contentDescription = R.string.description_download.toString()
)
}
IconButton(onClick = {
uiState.selectPost?.let {
share(it.sampleUrl, context)
}
}) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = R.string.description_share.toString(),
tint = Color.White
)
}
IconButton(onClick = {
uiState.selectPost?.let {
setWallpaper(it.sampleUrl, context)
}
}) {
Icon(
painter = painterResource(id = R.drawable.ic_twotone_wallpaper_24),
contentDescription = R.string.description_use_to_wallpaper.toString(),
tint = Color.White
)
}
},
)
}
}
}
@@ -0,0 +1,282 @@
package moe.uni.view.ui.compose.screen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
import moe.uni.view.bean.Post
import moe.uni.view.common.Config
import moe.uni.view.ui.compose.NAV_ROUTE_DETAIL_SCREEN
import moe.uni.view.ui.compose.PostViewModel
import moe.uni.view.ui.compose.widget.SearchBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class
)
@Composable
fun PostListScreen(
navController: NavController, viewModel: PostViewModel
) {
val postList by viewModel.postData.collectAsState()
val listState = rememberLazyStaggeredGridState()
val uiState by viewModel.uiState.collectAsState()
var lastScrollOffset by remember {
mutableIntStateOf(0)
}
var lastScrollPosition by remember {
mutableIntStateOf(0)
}
/**
* 1 scroll up
* -1 scroll down
* 0 default
*/
var scrollDirectionState by remember {
mutableIntStateOf(0)
}
val scope = rememberCoroutineScope()
var searchBarHeightSize by remember {
mutableStateOf(0.dp)
}
val density = LocalDensity.current
val searchBarPadding by remember {
mutableStateOf(24.dp)
}
var showBottomSheet by remember { mutableStateOf(false) }
val refreshing by remember {
uiState.refresh
}
val pullRefreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = {
scope.launch(Dispatchers.IO) {
viewModel.fetchPost(refresh = true)
}
})
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo }.map { layoutInfo ->
if (lastScrollOffset < listState.firstVisibleItemScrollOffset && lastScrollPosition < listState.firstVisibleItemIndex) {
scrollDirectionState = -1
}
if (lastScrollOffset > listState.firstVisibleItemScrollOffset && lastScrollPosition > listState.firstVisibleItemIndex) {
scrollDirectionState = 1
}
lastScrollOffset = listState.firstVisibleItemScrollOffset
lastScrollPosition = listState.firstVisibleItemIndex
layoutInfo.visibleItemsInfo.lastOrNull()?.index == postList.lastIndex
}.distinctUntilChanged().collect { reachedEnd ->
//避免初次加载数据时,list未绘制导致被判断为到达底部
if (reachedEnd && listState.firstVisibleItemIndex != 0) {
withContext(Dispatchers.IO) {
viewModel.fetchPost()
}
}
}
}
Box(modifier = Modifier.fillMaxSize()) {
LazyVerticalStaggeredGrid(
state = listState,
columns = StaggeredGridCells.Adaptive(200.dp),
verticalItemSpacing = 4.dp,
horizontalArrangement = Arrangement.spacedBy(4.dp),
content = {
//空置一行显示search bar
item(key = 0, span = StaggeredGridItemSpan.FullLine) {
Box(
modifier = Modifier
.height(searchBarHeightSize + searchBarPadding)
.fillMaxWidth()
)
}
items(items = postList, key = {
it.postId//提供key以保持列表顺序稳定
}) {
Row(Modifier.animateItemPlacement(tween(durationMillis = 250))) {
PostItem(it, clickable = {
viewModel.uiState.value.selectPost = it
navController.navigate(NAV_ROUTE_DETAIL_SCREEN)
})
}
}
},
modifier = Modifier
.background(Color.Transparent)
.pullRefresh(pullRefreshState)
.padding(horizontal = 4.dp)
)
PullRefreshIndicator(
refreshing, pullRefreshState,
Modifier
.align(Alignment.TopCenter)
.padding(searchBarHeightSize + searchBarPadding)
)
AnimatedVisibility(
visible = scrollDirectionState != -1,
enter = slideInVertically(
// 进入动画,从顶部滑下
initialOffsetY = { -it },
animationSpec = tween(durationMillis = 300)
),
exit = slideOutVertically(
// 退出动画,向上滑动消失
targetOffsetY = { -it },
animationSpec = tween(durationMillis = 300)
)
) {
SearchBar(
modifier = Modifier
.padding(horizontal = 24.dp, vertical = searchBarPadding / 2)
.fillMaxWidth()
.wrapContentHeight()
.onGloballyPositioned {
searchBarHeightSize = with(density) {
it.size.height.toDp()
}
},
searchTarget = uiState.searchTarget.value,
searchEvent = {
scope.launch(Dispatchers.IO) {
viewModel.fetchPost(it, refresh = true)
listState.animateScrollToItem(index = 0)
}
}, menuButtonAction = {
showBottomSheet = true
}
)
}
if (showBottomSheet) {
var safeModel by remember {
mutableStateOf(Config.getSafeMode())
}
ModalBottomSheet(
onDismissRequest = { showBottomSheet = false },
modifier = Modifier.height(400.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Safe mode",
modifier = Modifier.wrapContentSize(),
style = MaterialTheme.typography.bodyLarge
)
Switch(checked = safeModel, onCheckedChange = {
safeModel = it
Config.setSafeMode(it)
})
}
MaterialTheme.shapes.large
}
}
}
}
@Composable
fun PostItem(post: Post, clickable: (Post) -> Unit) {
Card {
SubcomposeAsyncImage(model = post.sampleUrl,
contentScale = ContentScale.FillWidth,
contentDescription = null,
loading = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
) {
CircularProgressIndicator(
modifier = Modifier
.size(50.dp)
.align(Alignment.Center)
)
}
},
success = {
SubcomposeAsyncImageContent()
},
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable { clickable.invoke(post) })
}
}
@@ -0,0 +1,11 @@
package moe.uni.view.ui.compose.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,70 @@
package moe.uni.view.ui.compose.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun LspViewTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package moe.uni.view.ui.compose.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -0,0 +1,86 @@
package moe.uni.view.ui.compose.widget
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun SearchBar(modifier: Modifier = Modifier,searchTarget: String = "", searchEvent: (String) -> Unit,menuButtonAction: () -> Unit) {
Row(
modifier = modifier
.background(MaterialTheme.colorScheme.onPrimary, shape = RoundedCornerShape(56.dp)),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = null,
modifier = Modifier
.padding(15.dp)
.height(30.dp)
.width(30.dp)
.clickable {
menuButtonAction.invoke()
}
)
var input: String by remember { mutableStateOf(searchTarget) }
val keyboardController = LocalSoftwareKeyboardController.current
val forceManager = LocalFocusManager.current
BasicTextField(
value = input,
onValueChange = {
input = it
},
singleLine = true,
textStyle = TextStyle(fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurfaceVariant),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions {
keyboardController?.hide()
forceManager.clearFocus()
searchEvent.invoke(input)
},
modifier = Modifier
.weight(1f)
.wrapContentHeight()
.fillMaxWidth(),
)
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
modifier = Modifier
.padding(15.dp)
.height(30.dp)
.width(30.dp)
)
}
}
@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
<path android:fillColor="@android:color/white" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>
@@ -0,0 +1,6 @@
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#fff"/>
<corners android:radius="5dp"/>
</shape>
@@ -1,5 +0,0 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<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>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>
@@ -1,4 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
@@ -1,5 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17.63,5.84C17.27,5.33 16.67,5 16,5L5,5.01C3.9,5.01 3,5.9 3,7v10c0,1.1 0.9,1.99 2,1.99L16,19c0.67,0 1.27,-0.33 1.63,-0.84L22,12l-4.37,-6.16z"/>
</vector>
@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,7L9.6,8.4l2.6,2.6H2v2h10.2l-2.6,2.6L11,17l5,-5L11,7zM20,19h-8v2h8c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2h-8v2h8V19z"/>
</vector>
@@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="?attr/colorPrimary"
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
@@ -1,5 +0,0 @@
<vector android:height="40dp" android:tint="#FFFFFF"
android:viewportHeight="20" android:viewportWidth="20"
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
</vector>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/colorAccent"/>
<corners android:radius="5dp"/>
<padding android:left="10dp"
android:right="10dp"
android:top="10dp"
android:bottom="10dp"/>
</shape>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/colorPrimary"/>
<corners android:radius="5dp"/>
<padding android:left="10dp"
android:right="10dp"
android:top="10dp"
android:bottom="10dp"/>
</shape>
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.favtag.FavTagActivity"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:fitsSystemWindows="true"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
app:title="@string/title_activity_favouriteTag"
/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/tag_recy"/>
</LinearLayout>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
tools:context=".activity.login.LoginActivity">
<TextView
android:layout_marginTop="160dp"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="44sp"
android:id="@+id/title"
android:text="@string/activity_login_login"/>
<EditText
android:hint="@string/activity_login_account"
android:layout_marginTop="20dp"
android:layout_gravity="center"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:id="@+id/username"/>
<EditText
android:hint="@string/activity_login_password"
android:inputType="textPassword"
android:layout_gravity="center"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:id="@+id/password"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="60dp"
android:layout_height="60dp"
android:id="@+id/login"
android:background="@color/primary"
android:src="@drawable/ic_baseline_navigate_next_24"
android:layout_marginTop="30dp"
android:layout_gravity="center"
android:elevation="8dp"/>
</LinearLayout>
-116
View File
@@ -1,116 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawerLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.main.MainActivity">
<RelativeLayout
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.main.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:fitsSystemWindows="true"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/toolbar_menu"
android:id="@+id/toolbar"
app:layout_scrollFlags="scroll|enterAlways|snap"
/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/swipeRefreshLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/recyclerview"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<LinearLayout
android:layout_marginTop="?attr/actionBarSize"
android:paddingTop="40dp"
android:visibility="gone"
android:id="@+id/search_bar"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<com.google.android.material.card.MaterialCardView
android:layout_width="0dp"
android:layout_weight="20"
android:elevation="15dp"
app:cardCornerRadius="3dp"
android:id="@+id/editCard"
style="?attr/materialCardViewElevatedStyle"
android:layout_height="50dp"
>
<EditText
android:background="@null"
android:hint="搜索..."
android:imeOptions="actionSearch"
android:singleLine="true"
android:paddingStart="10dp"
android:textSize="15sp"
android:layout_width="match_parent"
android:layout_height="50dp"
android:id="@+id/search"/>
<View
android:id="@+id/close"
android:layout_gravity="end"
android:layout_width="35dp"
android:layout_margin="10dp"
android:layout_height="35dp"
app:tint="?attr/colorPrimary"
android:background="@drawable/ic_baseline_close_24"/>
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
</RelativeLayout>
<com.google.android.material.navigation.NavigationView
android:layout_width="240dp"
android:layout_height="match_parent"
android:id="@+id/nav"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"
/>
<!-- TODO 侧边连:登录 收藏 Tag列表-->
</androidx.drawerlayout.widget.DrawerLayout>
-165
View File
@@ -1,165 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android" >
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="match_parent"
tools:context=".activity.pic.PicActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:id="@+id/appbar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/ctl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"/>
<ImageView
app:layout_collapseMode="parallax"
android:fitsSystemWindows="true"
android:id="@+id/titleImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="@string/appbar_scrolling_view_behavior"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</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">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
app:cardCornerRadius="10dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:layout_marginStart="5dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/idRecyclerView"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_marginStart="5dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tagRecyclerView"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_marginStart="5dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/authorRecyclerView"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_marginStart="5dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/sizeRecyclerView"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout
android:id="@+id/float_line"
android:layout_width="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical"
android:layout_margin="15dp"
android:layout_gravity="end|bottom"
android:layout_height="wrap_content">
<LinearLayout
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/ctrl"
android:orientation="vertical">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:elevation="5dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@drawable/ic_twotone_arrow_downward_24"
android:id="@+id/download"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="5dp"
android:layout_margin="10dp"
android:src="@drawable/ic_twotone_share_24"
android:id="@+id/share"/>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="80dp"
android:layout_height="80dp"
app:fabCustomSize="80dp"
android:layout_margin="10dp"
android:src="@drawable/ic_baseline_add_24"
android:elevation="5dp"
android:id="@+id/float_btn"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.github.chrisbanes.photoview.PhotoView
android:background="#000"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:id="@+id/photoView"/>
<ImageView
android:layout_margin="30dp"
android:layout_width="30dp"
android:layout_height="30dp"
android:id="@+id/back"
android:src="@drawable/ic_baseline_arrow_back_24"
android:layout_gravity="top|start"
/>
</FrameLayout>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fav_tag"
android:layout_margin="5dp"
android:padding="5dp"
android:textSize="20sp"
android:textColor="#fff"
android:background="@drawable/item_bg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:elevation="20dp"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="4dp"
android:layout_margin="5dp"
style="?attr/materialCardViewElevatedStyle"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/item_pic"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:id="@+id/picImgae"/>
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_header"
android:layout_width="match_parent"
android:layout_height="30dp">
</FrameLayout>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:fitsSystemWindows="true"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
/>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textAlignment="inherit"
android:textSize="20sp"/>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:padding="5dp"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:padding="5dp"
android:textSize="20sp"
android:textColor="#fff"
android:background="@drawable/item_bg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tag"/>
</LinearLayout>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/photo"
android:icon="@drawable/ic_baseline_insert_photo_24"
android:title="@string/item_gallery"/>
<item
android:id="@+id/taglist"
android:icon="@drawable/ic_baseline_label_24"
android:title="@string/item_collect"/>
<item
android:id="@+id/setting"
android:icon="@drawable/setting_icon"
android:title="@string/item_settings"/>
</group>
</menu>
-10
View File
@@ -1,10 +0,0 @@
<?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/search_nav"
android:icon="@drawable/ic_baseline_search_24"
app:showAsAction="always"
android:title="@string/item_search" />
</menu>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
+4 -2
View File
@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title_activity_settings">设置</string>
<string name="title_activity_favouriteTag">收藏标签</string>
<string name="item_gallery">画廊</string>
<string name="item_collect">收藏</string>
<string name="item_settings">设置</string>
<string name="item_search">搜索</string>
<string name="description_download">下载</string>
<string name="description_share">分享</string>
<string name="description_use_to_wallpaper">用作壁纸</string>
<string name="description_back">返回</string>
<string name="activity_login_login">登录</string>
<string name="activity_login_account">账户</string>
<string name="activity_login_password">密码</string>
@@ -24,8 +25,9 @@
<string name="toast_tag_exist">收藏过了哦</string>
<string name="toast_download_success">下载成功</string>
<string name="toast_download_fail">下载失败</string>
<string name="toast_compar_md5_fail">MD5对比失败</string>
<string name="toast_file_exist">文件已经存在</string>
<string name="toast_download_start">开始下载</string>
<string name="title_share">分享</string>
<string name="title_collect">收藏</string>
<string name="tags">标签</string>
</resources>
-12
View File
@@ -1,12 +0,0 @@
<resources>
<!-- Reply Preference -->
<string-array name="reply_entries">
<item>Reply</item>
<item>Reply to all</item>
</string-array>
<string-array name="reply_values">
<item>reply</item>
<item>reply_all</item>
</string-array>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
+4 -2
View File
@@ -2,13 +2,14 @@
<string name="app_name" translatable="false">YandView</string>
<string name="title_activity_settings">SettingsActivity</string>
<string name="title_activity_settings_backup" translatable="false">SettingsActivity_backup</string>
<string name="title_activity_favouriteTag">FavouriteTag</string>
<string name="item_gallery">Gallery</string>
<string name="item_collect">Collect</string>
<string name="item_settings">Settings</string>
<string name="item_search">Search</string>
<string name="description_download">Download</string>
<string name="description_share">Share</string>
<string name="description_use_to_wallpaper">Use to wallpaper</string>
<string name="description_back">Back</string>
<string name="activity_login_login">Login</string>
<string name="activity_login_account">Account</string>
<string name="activity_login_password">Password</string>
@@ -25,8 +26,9 @@
<string name="toast_tag_exist">This tag exist</string>
<string name="toast_download_success">Successfully download</string>
<string name="toast_download_fail">Download failed</string>
<string name="toast_compar_md5_fail">MD5 comparison failed</string>
<string name="toast_file_exist">File exists</string>
<string name="toast_download_start">Start download</string>
<string name="title_share">Share</string>
<string name="title_collect">Collect</string>
<string name="tags">Tags</string>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Material3.DayNight.NoActionBar" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
+6 -7
View File
@@ -1,14 +1,13 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/title_setting_load_source"
>
<PreferenceCategory app:title="@string/title_setting_load_source">
<ListPreference
app:defaultValue="yande.re"
app:entries="@array/pic_source"
app:entryValues="@array/pic_source"
app:key="sourceName"
app:key="source_name"
app:title="@string/setting_item_select_source"
app:useSimpleSummaryProvider="true" />
@@ -16,8 +15,8 @@
<PreferenceCategory app:title="@string/title_setting_safe_check">
<CheckBoxPreference
app:defaultValue="true"
app:title="@string/setting_item_safe_model"
app:key="safe_mode"/>
app:key="safe_mode"
app:title="@string/setting_item_safe_model" />
</PreferenceCategory>
+9 -3
View File
@@ -1,19 +1,25 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.31"
ext.kotlin_version = "1.8.10"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10'
classpath 'com.android.tools.build:gradle:8.3.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
plugins {
id 'com.google.devtools.ksp' version '1.9.20-1.0.14' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
}
allprojects {
repositories {
maven { url "https://www.jitpack.io" }
+1 -1
View File
@@ -1,6 +1,6 @@
#Mon Sep 12 00:20:10 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

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