diff --git a/app/src/main/java/ceui/lisa/activities/TemplateActivity.java b/app/src/main/java/ceui/lisa/activities/TemplateActivity.java index 313390289..beed72d94 100644 --- a/app/src/main/java/ceui/lisa/activities/TemplateActivity.java +++ b/app/src/main/java/ceui/lisa/activities/TemplateActivity.java @@ -79,7 +79,6 @@ import ceui.lisa.utils.ReverseResult; import ceui.loxia.FlagDescFragment; import ceui.loxia.FlagReasonFragment; -import ceui.loxia.ObjectSpec; public class TemplateActivity extends BaseActivity implements ColorPickerDialogListener { diff --git a/app/src/main/java/ceui/lisa/activities/UActivity.java b/app/src/main/java/ceui/lisa/activities/UActivity.java deleted file mode 100644 index 49ec51663..000000000 --- a/app/src/main/java/ceui/lisa/activities/UActivity.java +++ /dev/null @@ -1,279 +0,0 @@ -package ceui.lisa.activities; - -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.view.ViewTreeObserver; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; - -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; - -import com.bumptech.glide.Glide; -import com.github.ybq.android.spinkit.style.Wave; -import com.google.android.material.appbar.AppBarLayout; -import com.qmuiteam.qmui.skin.QMUISkinManager; -import com.qmuiteam.qmui.widget.dialog.QMUIDialog; - -import ceui.lisa.R; -import ceui.lisa.database.AppDatabase; -import ceui.lisa.database.MuteEntity; -import ceui.lisa.databinding.ActivityNewUserBinding; -import ceui.lisa.fragments.FragmentHolder; -import ceui.lisa.http.NullCtrl; -import ceui.lisa.http.Retro; -import ceui.lisa.interfaces.Display; -import ceui.lisa.models.UserDetailResponse; -import ceui.lisa.models.UserFollowDetail; -import ceui.lisa.utils.Common; -import ceui.lisa.utils.GlideUtil; -import ceui.lisa.utils.Params; -import ceui.lisa.utils.PixivOperate; -import ceui.lisa.viewmodel.AppLevelViewModel; -import ceui.lisa.viewmodel.UserViewModel; -import ceui.loxia.Event; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class UActivity extends BaseActivity implements Display { - - private int userID; - private UserViewModel mUserViewModel; - - @Override - protected int initLayout() { - return R.layout.activity_new_user; - } - - @Override - protected void initView() { - Wave wave = new Wave(); - baseBind.progress.setIndeterminateDrawable(wave); - baseBind.toolbar.setPadding(0, Shaft.statusHeight, 0, 0); - baseBind.toolbar.setNavigationOnClickListener(v -> finish()); - baseBind.toolbarLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - final int offset = baseBind.toolbarLayout.getHeight() - Shaft.statusHeight - Shaft.toolbarHeight; - baseBind.appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() { - @Override - public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { - if (Math.abs(verticalOffset) < 15) { - baseBind.centerHeader.setAlpha(1.0f); - baseBind.toolbarTitle.setAlpha(0.0f); - } else if ((offset - Math.abs(verticalOffset)) < 15) { - baseBind.centerHeader.setAlpha(0.0f); - baseBind.toolbarTitle.setAlpha(1.0f); - } else { - baseBind.centerHeader.setAlpha(1 + (float) verticalOffset / offset); - baseBind.toolbarTitle.setAlpha(-(float) verticalOffset / offset); - } - Common.showLog(className + verticalOffset); - } - }); - baseBind.toolbarLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - }); - } - - @Override - protected void initBundle(Bundle bundle) { - userID = bundle.getInt(Params.USER_ID); - } - - @Override - public void initModel() { - mUserViewModel = new ViewModelProvider(this).get(UserViewModel.class); - mUserViewModel.getUser().observe(this, new Observer() { - @Override - public void onChanged(UserDetailResponse userDetailResponse) { - invoke(userDetailResponse); - } - }); - final MuteEntity entity = AppDatabase.getAppDatabase(this).searchDao().getMuteEntityByID(userID); - mUserViewModel.isUserMuted.setValue(entity != null); - - final MuteEntity block = AppDatabase.getAppDatabase(this).searchDao().getBlockMuteEntityByID(userID); - mUserViewModel.isUserBlocked.setValue(block != null); - - Shaft.appViewModel.getFollowUserLiveData(userID).observe(this, new Observer() { - @Override - public void onChanged(Integer integer) { - updateFollowUserUI(integer); - } - }); - } - - @Override - protected void initData() { - baseBind.progress.setVisibility(View.VISIBLE); - Retro.getAppApi().getUserDetail(Shaft.sUserModel.getAccess_token(), userID) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new NullCtrl() { - @Override - public void success(UserDetailResponse user) { - mUserViewModel.getUser().setValue(user); - Shaft.appViewModel.updateFollowUserStatus(userID, user.getUser().isIs_followed() ? AppLevelViewModel.FollowUserStatus.FOLLOWED : AppLevelViewModel.FollowUserStatus.NOT_FOLLOW); - } - - @Override - public void must() { - baseBind.progress.setVisibility(View.INVISIBLE); - } - }); - Retro.getAppApi().getFollowDetail(Shaft.sUserModel.getAccess_token(), userID) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new NullCtrl() { - @Override - public void success(UserFollowDetail userFollowDetail) { - //mUserViewModel.getUserFollowDetail().setValue(userFollowDetail); - int followStatus = AppLevelViewModel.FollowUserStatus.NOT_FOLLOW; - if (userFollowDetail.isPublicFollow()) { - followStatus = AppLevelViewModel.FollowUserStatus.FOLLOWED_PUBLIC; - } else if (userFollowDetail.isPrivateFollow()) { - followStatus = AppLevelViewModel.FollowUserStatus.FOLLOWED_PRIVATE; - } else if (userFollowDetail.isFollow()) { - followStatus = AppLevelViewModel.FollowUserStatus.FOLLOWED; - } - Shaft.appViewModel.updateFollowUserStatus(userID, followStatus); - } - }); - } - - @Override - public boolean hideStatusBar() { - return true; - } - - @Override - public void invoke(UserDetailResponse data) { - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.fragment_container, FragmentHolder.newInstance()) - .commitNowAllowingStateLoss(); - - if (userID == Shaft.sUserModel.getUserId()) { - baseBind.starUser.setVisibility(View.INVISIBLE); - baseBind.moreAction.setVisibility(View.GONE); - } else { - baseBind.starUser.setVisibility(View.VISIBLE); - baseBind.moreAction.setVisibility(View.VISIBLE); - baseBind.moreAction.setOnClickListener(v -> { - final boolean isMuted = Boolean.TRUE.equals(mUserViewModel.isUserMuted.getValue()); - String[] OPTIONS = new String[] { - isMuted ? getString(R.string.cancel_block_this_users_work) : getString(R.string.block_this_users_work), -// getString(R.string.add_user_to_blacklist) - }; - new QMUIDialog.MenuDialogBuilder(mActivity) - .setSkinManager(QMUISkinManager.defaultInstance(mActivity)) - .addItems(OPTIONS, (dialog, which) -> { - if (which == 0) { - if (isMuted) { - PixivOperate.unMuteUser(data.getUser()); - mUserViewModel.isUserMuted.setValue(false); - } else { - PixivOperate.muteUser(data.getUser()); - mUserViewModel.isUserMuted.setValue(true); - } - mUserViewModel.refreshEvent.setValue(new Event<>(100, 0L)); - } else if (which == 1) { - - } - dialog.dismiss(); - }) - .show(); - }); - - baseBind.starUser.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Integer integerValue = Shaft.appViewModel.getFollowUserLiveData(userID).getValue(); - if (AppLevelViewModel.FollowUserStatus.isFollowed(integerValue)) { - PixivOperate.postUnFollowUser(data.getUser().getId()); - data.getUser().setIs_followed(false); - } else { - PixivOperate.postFollowUser(data.getUser().getId(), Params.TYPE_PUBLIC); - data.getUser().setIs_followed(true); - } - } - }); - baseBind.starUser.setOnLongClickListener(v1 -> { - Integer integerValue = Shaft.appViewModel.getFollowUserLiveData(userID).getValue(); - if (!AppLevelViewModel.FollowUserStatus.isFollowed(integerValue)) { - data.getUser().setIs_followed(true); - } - PixivOperate.postFollowUser(data.getUser().getId(), Params.TYPE_PRIVATE); - return true; - }); - } - - baseBind.centerHeader.setVisibility(View.VISIBLE); - Animation animation = new AlphaAnimation(0.0f, 1.0f); - animation.setDuration(800L); - baseBind.centerHeader.startAnimation(animation); - if (data.getUser().isIs_premium()) { - baseBind.vipImage.setVisibility(View.VISIBLE); - } else { - baseBind.vipImage.setVisibility(View.GONE); - } - Glide.with(mContext).load(GlideUtil.getHead(data.getUser())).into(baseBind.userHead); - baseBind.userName.setText(data.getUser().getName()); - baseBind.userName.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Common.copy(mContext, String.valueOf(data.getUser().getId())); - } - }); - baseBind.userName.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - Common.copy(mContext, data.getUser().getName()); - return true; - } - }); - - baseBind.follow.setText(String.valueOf(data.getProfile().getTotal_follow_users())); - baseBind.pFriend.setText(String.valueOf(data.getProfile().getTotal_mypixiv_users())); - - View.OnClickListener pFriend = new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(Params.USER_ID, data.getUser().getId()); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "好P友"); - startActivity(intent); - } - }; - baseBind.pFriend.setOnClickListener(pFriend); - baseBind.pFriendS.setOnClickListener(pFriend); - - View.OnClickListener follow = new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(Params.USER_ID, data.getUser().getId()); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "正在关注"); - startActivity(intent); - } - }; - baseBind.follow.setOnClickListener(follow); - baseBind.followS.setOnClickListener(follow); - } - - private void updateFollowUserUI(int status) { - if (AppLevelViewModel.FollowUserStatus.isFollowed(status)) { - baseBind.starUser.setText(R.string.string_177); - if (AppLevelViewModel.FollowUserStatus.isPrivateFollowed(status)) { - baseBind.starUser.setBackgroundResource(R.drawable.follow_button_stroke_new_dotted); - } else { - baseBind.starUser.setBackgroundResource(R.drawable.follow_button_stroke_new); - } - } else { - baseBind.starUser.setText(R.string.string_178); - baseBind.starUser.setBackgroundResource(R.drawable.follow_button_stroke_new); - } - } -} diff --git a/app/src/main/java/ceui/lisa/activities/UActivity.kt b/app/src/main/java/ceui/lisa/activities/UActivity.kt new file mode 100644 index 000000000..345a6b6c0 --- /dev/null +++ b/app/src/main/java/ceui/lisa/activities/UActivity.kt @@ -0,0 +1,290 @@ +package ceui.lisa.activities + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import ceui.lisa.R +import ceui.lisa.database.AppDatabase +import ceui.lisa.databinding.ActivityNewUserBinding +import ceui.lisa.fragments.FragmentHolder.Companion.newInstance +import ceui.lisa.http.NullCtrl +import ceui.lisa.http.Retro +import ceui.lisa.interfaces.Display +import ceui.lisa.models.UserBean +import ceui.lisa.models.UserDetailResponse +import ceui.lisa.models.UserFollowDetail +import ceui.lisa.utils.Common +import ceui.lisa.utils.GlideUtil +import ceui.lisa.utils.Params +import ceui.lisa.utils.PixivOperate +import ceui.lisa.viewmodel.AppLevelViewModel +import ceui.lisa.viewmodel.UserViewModel +import ceui.loxia.Client +import ceui.loxia.Event +import ceui.loxia.ObjectPool +import ceui.loxia.ProgressTextButton +import ceui.refactor.setOnClick +import com.bumptech.glide.Glide +import com.github.ybq.android.spinkit.style.Wave +import com.qmuiteam.qmui.skin.QMUISkinManager +import com.qmuiteam.qmui.widget.dialog.QMUIDialog.MenuDialogBuilder +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class UActivity : BaseActivity(), Display { + private var userId = 0 + private lateinit var mUserViewModel: UserViewModel + override fun initLayout(): Int { + return R.layout.activity_new_user + } + + override fun initView() { + val wave = Wave() + baseBind.progress.indeterminateDrawable = wave + baseBind.toolbar.setPadding(0, Shaft.statusHeight, 0, 0) + baseBind.toolbar.setNavigationOnClickListener { v: View? -> finish() } + baseBind.toolbarLayout.viewTreeObserver.addOnGlobalLayoutListener(object : + OnGlobalLayoutListener { + override fun onGlobalLayout() { + val offset = + baseBind.toolbarLayout.height - Shaft.statusHeight - Shaft.toolbarHeight + baseBind.appBar.addOnOffsetChangedListener { appBarLayout, verticalOffset -> + if (Math.abs(verticalOffset) < 15) { + baseBind.centerHeader.alpha = 1.0f + baseBind.toolbarTitle.alpha = 0.0f + } else if (offset - Math.abs(verticalOffset) < 15) { + baseBind.centerHeader.alpha = 0.0f + baseBind.toolbarTitle.alpha = 1.0f + } else { + baseBind.centerHeader.alpha = 1 + verticalOffset.toFloat() / offset + baseBind.toolbarTitle.alpha = -verticalOffset.toFloat() / offset + } + Common.showLog(className + verticalOffset) + } + baseBind.toolbarLayout.viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }) + } + + override fun initBundle(bundle: Bundle) { + userId = bundle.getInt(Params.USER_ID) + } + + override fun initModel() { + mUserViewModel = ViewModelProvider(this)[UserViewModel::class.java] + mUserViewModel.user.observe(this) { userDetailResponse -> invoke(userDetailResponse) } + val entity = AppDatabase.getAppDatabase(this).searchDao().getUserMuteEntityByID(userId) + mUserViewModel.isUserMuted.value = entity != null + val block = AppDatabase.getAppDatabase(this).searchDao().getBlockMuteEntityByID(userId) + mUserViewModel.isUserBlocked.value = block != null + val userLiveData = ObjectPool.get(userId.toLong()) + userLiveData.observe(this) { user -> + updateUser(user) + Common.showLog("updateUser invoke ${user.isIs_followed}") + } + } + + private fun updateUser(user: UserBean) { + if (user.isIs_followed) { + baseBind.follow.isVisible = false + baseBind.unfollow.isVisible = true + baseBind.unfollow.setOnClick { + unfollowUser(it, userId) + } + baseBind.unfollow.setOnLongClickListener { + true + } + } else { + baseBind.unfollow.isVisible = false + baseBind.follow.isVisible = true + baseBind.follow.setOnClick { + followUser(it, userId, Params.TYPE_PUBLIC) + } + baseBind.follow.setOnLongClickListener { + followUser(it as ProgressTextButton, userId, Params.TYPE_PRIVATE) + true + } + } + } + + override fun initData() { + baseBind.progress.visibility = View.VISIBLE + Retro.getAppApi().getUserDetail(Shaft.sUserModel.access_token, userId) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : NullCtrl() { + override fun success(user: UserDetailResponse) { + mUserViewModel.user.value = user + Shaft.appViewModel.updateFollowUserStatus( + userId, + if (user.user.isIs_followed) AppLevelViewModel.FollowUserStatus.FOLLOWED else AppLevelViewModel.FollowUserStatus.NOT_FOLLOW + ) + } + + override fun must() { + baseBind.progress.visibility = View.INVISIBLE + } + }) + Retro.getAppApi().getFollowDetail(Shaft.sUserModel.access_token, userId) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : NullCtrl() { + override fun success(userFollowDetail: UserFollowDetail) { + //mUserViewModel.getUserFollowDetail().setValue(userFollowDetail); + var followStatus = AppLevelViewModel.FollowUserStatus.NOT_FOLLOW + if (userFollowDetail.isPublicFollow) { + followStatus = AppLevelViewModel.FollowUserStatus.FOLLOWED_PUBLIC + } else if (userFollowDetail.isPrivateFollow) { + followStatus = AppLevelViewModel.FollowUserStatus.FOLLOWED_PRIVATE + } else if (userFollowDetail.isFollow) { + followStatus = AppLevelViewModel.FollowUserStatus.FOLLOWED + } + Shaft.appViewModel.updateFollowUserStatus(userId, followStatus) + } + }) + } + + override fun hideStatusBar(): Boolean { + return true + } + + override operator fun invoke(data: UserDetailResponse) { + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, newInstance()) + .commitNowAllowingStateLoss() + if (userId == Shaft.sUserModel.userId) { + baseBind.followLayout.visibility = View.GONE + baseBind.moreAction.visibility = View.GONE + } else { + baseBind.followLayout.visibility = View.VISIBLE + baseBind.moreAction.visibility = View.VISIBLE + baseBind.moreAction.setOnClickListener { v: View? -> + val isMuted = java.lang.Boolean.TRUE == mUserViewModel.isUserMuted.value + val OPTIONS = arrayOf( + if (isMuted) getString(R.string.cancel_block_this_users_work) else getString(R.string.block_this_users_work) + ) + MenuDialogBuilder(mActivity) + .setSkinManager(QMUISkinManager.defaultInstance(mActivity)) + .addItems(OPTIONS) { dialog: DialogInterface, which: Int -> + if (which == 0) { + if (isMuted) { + PixivOperate.unMuteUser(data.user) + mUserViewModel.isUserMuted.setValue(false) + } else { + PixivOperate.muteUser(data.user) + mUserViewModel.isUserMuted.setValue(true) + } + mUserViewModel.refreshEvent.setValue(Event(100, 0L)) + } else if (which == 1) { + } + dialog.dismiss() + } + .show() + } + } + baseBind.centerHeader.visibility = View.VISIBLE + val animation: Animation = AlphaAnimation(0.0f, 1.0f) + animation.duration = 800L + baseBind.centerHeader.startAnimation(animation) + if (data.user.isIs_premium) { + baseBind.vipImage.visibility = View.VISIBLE + } else { + baseBind.vipImage.visibility = View.GONE + } + Glide.with(mContext).load(GlideUtil.getHead(data.user)).into(baseBind.userHead) + baseBind.userName.text = data.user.name + baseBind.userName.setOnClickListener { Common.copy(mContext, data.user.id.toString()) } + baseBind.userName.setOnLongClickListener { + Common.copy(mContext, data.user.name) + true + } + baseBind.followCount.text = data.profile.total_follow_users.toString() + baseBind.pFriend.text = data.profile.total_mypixiv_users.toString() + val pFriend = View.OnClickListener { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(Params.USER_ID, data.user.id) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "好P友") + startActivity(intent) + } + baseBind.pFriend.setOnClickListener(pFriend) + baseBind.pFriendS.setOnClickListener(pFriend) + val follow = View.OnClickListener { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(Params.USER_ID, data.user.id) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "正在关注") + startActivity(intent) + } + baseBind.follow.setOnClickListener(follow) + baseBind.followS.setOnClickListener(follow) + } +} + +fun Fragment.followUser(sender: ProgressTextButton, userId: Int, followType: String) { + activity?.followUser(sender, userId, followType) +} + +fun FragmentActivity.followUser(sender: ProgressTextButton, userId: Int, followType: String) { + lifecycleScope.launch { + try { + sender.showProgress() + Client.appApi.postFollow(userId.toLong(), followType) + delay(500L) + ObjectPool.followUser(userId.toLong()) + if (followType == Params.TYPE_PUBLIC) { + Shaft.appViewModel.updateFollowUserStatus( + userId, + AppLevelViewModel.FollowUserStatus.FOLLOWED_PUBLIC + ) + Common.showToast(getString(R.string.like_success_public)) + } else { + Shaft.appViewModel.updateFollowUserStatus( + userId, + AppLevelViewModel.FollowUserStatus.FOLLOWED_PRIVATE + ) + Common.showToast(getString(R.string.like_success_private)) + } + } catch (ex: Exception) { + ex.printStackTrace() + Common.showToast(ex.message) + } finally { + sender.hideProgress() + } + } +} + +fun Fragment.unfollowUser(sender: ProgressTextButton, userId: Int) { + activity?.unfollowUser(sender, userId) +} + +fun FragmentActivity.unfollowUser(sender: ProgressTextButton, userId: Int) { + lifecycleScope.launch { + try { + sender.showProgress() + Client.appApi.postUnFollow(userId.toLong()) + delay(500L) + Shaft.appViewModel.updateFollowUserStatus( + userId, + AppLevelViewModel.FollowUserStatus.NOT_FOLLOW + ) + ObjectPool.unFollowUser(userId.toLong()) + Common.showToast(getString(R.string.cancel_like)) + } catch (ex: Exception) { + ex.printStackTrace() + Common.showToast(ex.message) + } finally { + sender.hideProgress() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ceui/lisa/activities/VActivity.java b/app/src/main/java/ceui/lisa/activities/VActivity.java index dd02bc85d..1728f62df 100644 --- a/app/src/main/java/ceui/lisa/activities/VActivity.java +++ b/app/src/main/java/ceui/lisa/activities/VActivity.java @@ -64,7 +64,7 @@ public Fragment getItem(int position) { return FragmentSingleUgora.newInstance(illustsBean); } else { if (Shaft.sSettings.isUseFragmentIllust()) { - return FragmentIllust.newInstance(illustsBean); + return FragmentIllust.newInstance(illustsBean.getId()); } else { return FragmentSingleIllust.newInstance(illustsBean); } diff --git a/app/src/main/java/ceui/lisa/core/Mapper.java b/app/src/main/java/ceui/lisa/core/Mapper.java index 72bacd3c8..25c00e254 100644 --- a/app/src/main/java/ceui/lisa/core/Mapper.java +++ b/app/src/main/java/ceui/lisa/core/Mapper.java @@ -7,6 +7,7 @@ import ceui.lisa.interfaces.ListShow; import ceui.lisa.models.IllustsBean; import ceui.lisa.models.NovelBean; +import ceui.loxia.ObjectPool; import io.reactivex.functions.Function; /** @@ -27,6 +28,7 @@ public T apply(T t) { if (isTagBanned || isIdBanned || isUserBanned || isR18FilterBanned) { dash.add(o); } + ObjectPool.INSTANCE.updateIllust((IllustsBean) o); } if (o instanceof NovelBean) { boolean isTagBanned = IllustNovelFilter.judgeTag((NovelBean) o); diff --git a/app/src/main/java/ceui/lisa/database/DownloadDao.java b/app/src/main/java/ceui/lisa/database/DownloadDao.java index 7c73e16a6..0be192a9b 100644 --- a/app/src/main/java/ceui/lisa/database/DownloadDao.java +++ b/app/src/main/java/ceui/lisa/database/DownloadDao.java @@ -36,9 +36,6 @@ public interface DownloadDao { @Delete void delete(DownloadEntity userEntity); - @Delete - void deleteMuteEntity(MuteEntity muteEntity); - /** * 获取全部下载记录 * diff --git a/app/src/main/java/ceui/lisa/database/SearchDao.java b/app/src/main/java/ceui/lisa/database/SearchDao.java deleted file mode 100644 index 17262c6e7..000000000 --- a/app/src/main/java/ceui/lisa/database/SearchDao.java +++ /dev/null @@ -1,91 +0,0 @@ -package ceui.lisa.database; - -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Update; - -import java.util.List; - -@Dao -public interface SearchDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - void insert(SearchEntity searchEntity); - - @Query("SELECT * FROM search_table WHERE id = :id LIMIT 1") - SearchEntity getSearchEntity(int id); - - @Delete - void deleteSearchEntity(SearchEntity searchEntity); - - @Query("DELETE FROM search_table") - void deleteAll(); - - @Query("DELETE FROM search_table WHERE pinned = 0") - void deleteAllUnpinned(); - - @Query("SELECT * FROM search_table ORDER BY pinned DESC, searchTime DESC LIMIT :limit") - List getAll(int limit); - - @Query("SELECT * FROM search_table") - List getAllSearchEntities(); - - //添加一个屏蔽标签 - @Insert(onConflict = OnConflictStrategy.REPLACE) - void insertMuteTag(MuteEntity muteEntity); - - @Update - void updateMuteTag(MuteEntity muteEntity); - - //删除所有屏蔽的标签 - @Query("DELETE FROM tag_mute_table WHERE type = 0") - void deleteAllMutedTags(); - - @Query("DELETE FROM tag_mute_table WHERE type = 3") - void deleteAllMutedUsers(); - - @Query("DELETE FROM tag_mute_table WHERE type = 1 OR type = 2") - void deleteMutedWorks(); - - @Query("DELETE FROM tag_mute_table WHERE type = 3") - void deleteMutedUser(); - - @Query("SELECT * FROM tag_mute_table WHERE type = 0 ORDER BY searchTime DESC ") - List getAllMutedTags(); - - @Query("SELECT * FROM tag_mute_table WHERE type = 1 OR type = 2 ORDER BY searchTime DESC ") - List getMutedWorks(); - - @Query("SELECT * FROM tag_mute_table WHERE type = 1 ORDER BY searchTime DESC ") - List getMutedIllusts(); - - @Query("SELECT * FROM tag_mute_table WHERE type = 3 ORDER BY searchTime DESC LIMIT :limit OFFSET :offset") - List getMutedUser(int limit, int offset); - - @Query("SELECT * FROM tag_mute_table WHERE type = 3 AND id = :userID LIMIT 1") - MuteEntity getMuteEntityByID(int userID); - - @Query("SELECT * FROM tag_mute_table WHERE type = 4 AND id = :userID LIMIT 1") - MuteEntity getBlockMuteEntityByID(int userID); - - @Query("SELECT * FROM tag_mute_table") - List getAllMuteEntities(); - - @Delete - void unMuteTag(MuteEntity userEntity); - - - /** - * 添加一个列表记录 - * @param uuidEntity - */ - @Insert(onConflict = OnConflictStrategy.REPLACE) - void insertListWithUUID(UUIDEntity uuidEntity); - - - @Query("SELECT * FROM uuid_list_table WHERE uuid = :paramUUID LIMIT 1") - UUIDEntity getListByUUID(String paramUUID); -} diff --git a/app/src/main/java/ceui/lisa/database/SearchDao.kt b/app/src/main/java/ceui/lisa/database/SearchDao.kt new file mode 100644 index 000000000..0eb059b6e --- /dev/null +++ b/app/src/main/java/ceui/lisa/database/SearchDao.kt @@ -0,0 +1,91 @@ +package ceui.lisa.database + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface SearchDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(searchEntity: SearchEntity) + + @Query("SELECT * FROM search_table WHERE id = :id LIMIT 1") + fun getSearchEntity(id: Int): SearchEntity + + @Delete + fun deleteSearchEntity(searchEntity: SearchEntity) + + @Query("DELETE FROM search_table") + fun deleteAll() + + @Query("DELETE FROM search_table WHERE pinned = 0") + fun deleteAllUnpinned() + + @Query("SELECT * FROM search_table ORDER BY pinned DESC, searchTime DESC LIMIT :limit") + fun getAll(limit: Int): List + + @get:Query("SELECT * FROM search_table") + val allSearchEntities: List + + //添加一个屏蔽标签 + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertMuteTag(muteEntity: MuteEntity) + + @Update + fun updateMuteTag(muteEntity: MuteEntity) + + //删除所有屏蔽的标签 + @Query("DELETE FROM tag_mute_table WHERE type = 0") + fun deleteAllMutedTags() + + @Query("DELETE FROM tag_mute_table WHERE type = 3") + fun deleteAllMutedUsers() + + @Query("DELETE FROM tag_mute_table WHERE type = 1 OR type = 2") + fun deleteMutedWorks() + + @Query("DELETE FROM tag_mute_table WHERE type = 3") + fun deleteMutedUser() + + @get:Query("SELECT * FROM tag_mute_table WHERE type = 0 ORDER BY searchTime DESC ") + val allMutedTags: List + + @get:Query("SELECT * FROM tag_mute_table WHERE type = 1 OR type = 2 ORDER BY searchTime DESC ") + val mutedWorks: List + + @get:Query("SELECT * FROM tag_mute_table WHERE type = 1 ORDER BY searchTime DESC ") + val mutedIllusts: List + + @Query("SELECT * FROM tag_mute_table WHERE type = 3 ORDER BY searchTime DESC LIMIT :limit OFFSET :offset") + fun getMutedUser(limit: Int, offset: Int): List + + @Query("SELECT * FROM tag_mute_table WHERE type = 3 AND id = :userID LIMIT 1") + fun getUserMuteEntityByID(userID: Int): MuteEntity? + + @Query("SELECT * FROM tag_mute_table WHERE type = 3 AND id = :userID LIMIT 1") + fun getUserMuteEntityByIDLiveData(userID: Int): LiveData + + @Query("SELECT * FROM tag_mute_table WHERE type = 1 AND id = :illustId LIMIT 1") + fun getIllustMuteEntityByID(illustId: Int): LiveData + + @Query("SELECT * FROM tag_mute_table WHERE type = 4 AND id = :userID LIMIT 1") + fun getBlockMuteEntityByID(userID: Int): MuteEntity? + + @Delete + fun deleteMuteEntity(muteEntity: MuteEntity) + + @get:Query("SELECT * FROM tag_mute_table") + val allMuteEntities: List + + @Delete + fun unMuteTag(userEntity: MuteEntity) + + /** + * 添加一个列表记录 + * @param uuidEntity + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertListWithUUID(uuidEntity: UUIDEntity) + + @Query("SELECT * FROM uuid_list_table WHERE uuid = :paramUUID LIMIT 1") + fun getListByUUID(paramUUID: String): UUIDEntity +} \ No newline at end of file diff --git a/app/src/main/java/ceui/lisa/fragments/BaseFragment.java b/app/src/main/java/ceui/lisa/fragments/BaseFragment.java index db593bb0c..e094836a1 100644 --- a/app/src/main/java/ceui/lisa/fragments/BaseFragment.java +++ b/app/src/main/java/ceui/lisa/fragments/BaseFragment.java @@ -22,6 +22,7 @@ public abstract class BaseFragment extends Fragment { protected View rootView; + @NonNull protected Layout baseBind; protected String className = getClass().getSimpleName() + " "; diff --git a/app/src/main/java/ceui/lisa/fragments/FragmentIllust.java b/app/src/main/java/ceui/lisa/fragments/FragmentIllust.java deleted file mode 100644 index 8710ecb24..000000000 --- a/app/src/main/java/ceui/lisa/fragments/FragmentIllust.java +++ /dev/null @@ -1,577 +0,0 @@ -package ceui.lisa.fragments; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.Toolbar; -import androidx.lifecycle.Observer; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.recyclerview.widget.LinearLayoutManager; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.Target; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.qmuiteam.qmui.skin.QMUISkinManager; -import com.qmuiteam.qmui.widget.dialog.QMUIDialog; -import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction; -import com.scwang.smart.refresh.layout.SmartRefreshLayout; - -import com.zhy.view.flowlayout.FlowLayout; -import com.zhy.view.flowlayout.TagAdapter; -import com.zhy.view.flowlayout.TagFlowLayout; - -import ceui.lisa.R; -import ceui.lisa.activities.BaseActivity; -import ceui.lisa.activities.SearchActivity; -import ceui.lisa.activities.Shaft; -import ceui.lisa.activities.TemplateActivity; -import ceui.lisa.activities.UserActivity; -import ceui.lisa.adapters.IllustAdapter; -import ceui.lisa.database.SearchEntity; -import ceui.lisa.databinding.FragmentIllustBinding; -import ceui.lisa.dialogs.MuteDialog; -import ceui.lisa.download.IllustDownload; -import ceui.lisa.models.IllustsBean; -import ceui.lisa.models.TagsBean; -import ceui.lisa.notification.BaseReceiver; -import ceui.lisa.notification.CallBackReceiver; -import ceui.lisa.utils.Common; -import ceui.lisa.utils.DensityUtil; -import ceui.lisa.utils.GlideUrlChild; -import ceui.lisa.utils.GlideUtil; -import ceui.lisa.utils.Params; -import ceui.lisa.utils.PixivOperate; -import ceui.lisa.utils.ShareIllust; -import ceui.lisa.viewmodel.AppLevelViewModel; -import ceui.loxia.FlagDescFragment; -import ceui.loxia.ObjectSpec; -import ceui.loxia.test.ListActivity; - -import static ceui.lisa.utils.SearchTypeUtil.SEARCH_TYPE_DB_KEYWORD; -import static ceui.lisa.utils.ShareIllust.URL_Head; - - -public class FragmentIllust extends SwipeFragment { - - private IllustsBean illust; - - public static FragmentIllust newInstance(IllustsBean illustsBean) { - Bundle args = new Bundle(); - args.putSerializable(Params.CONTENT, illustsBean); - FragmentIllust fragment = new FragmentIllust(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void initBundle(Bundle bundle) { - illust = (IllustsBean) bundle.getSerializable(Params.CONTENT); - } - - @Override - public void initLayout() { - mLayoutID = R.layout.fragment_illust; - } - - @Override - protected void initView() { - if (illust.getId() == 0 || !illust.isVisible()) { - Common.showToast(R.string.string_206); - new Handler().postDelayed(this::finish, 1000); - return; - } - - if (illust.getSeries() != null && !TextUtils.isEmpty(illust.getSeries().getTitle())) { - ClickableSpan clickableSpan = new ClickableSpan() { - @Override - public void onClick(View widget) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "漫画系列详情"); - intent.putExtra(Params.MANGA_SERIES_ID, illust.getSeries().getId()); - startActivity(intent); - } - - @Override - public void updateDrawState(TextPaint ds) { - ds.setColor(Common.resolveThemeAttribute(mContext, R.attr.colorPrimary)); - } - }; - SpannableString spannableString; - String seriesString = getString(R.string.string_229); - spannableString = new SpannableString(String.format("@%s %s", - seriesString, illust.getTitle())); - spannableString.setSpan(clickableSpan, 0, seriesString.length() + 1, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - baseBind.title.setMovementMethod(LinkMovementMethod.getInstance()); - baseBind.title.setText(spannableString); - } else { - baseBind.title.setText(illust.getTitle()); - } - baseBind.title.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - Common.copy(mContext, illust.getTitle()); - return true; - } - }); - baseBind.toolbar.inflateMenu(R.menu.share); - baseBind.toolbar.setNavigationOnClickListener(v -> mActivity.finish()); - baseBind.toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - if (menuItem.getItemId() == R.id.action_share) { - new ShareIllust(mContext, illust) { - @Override - public void onPrepare() { - - } - }.execute(); - return true; - } else if (menuItem.getItemId() == R.id.action_share_image) { - Glide.with(mContext) - .asBitmap() - .load(new GlideUrlChild(IllustDownload.getUrl(illust, 0, Params.IMAGE_RESOLUTION_LARGE))) - .listener(new RequestListener() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { - return false; - } - - @Override - public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { - Uri uri = Common.copyBitmapToImageCacheFolder(resource, illust.getId() + ".png"); - if(uri != null){ - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - shareIntent.setDataAndType(uri, mContext.getContentResolver().getType(uri)); - shareIntent.putExtra(Intent.EXTRA_STREAM, uri); - startActivity(Intent.createChooser(shareIntent, getString(R.string.share))); - } - return true; - } - }).submit(); - - } else if (menuItem.getItemId() == R.id.action_dislike) { - MuteDialog muteDialog = MuteDialog.newInstance(illust); - muteDialog.show(getChildFragmentManager(), "MuteDialog"); - return true; - } else if (menuItem.getItemId() == R.id.action_copy_link) { - String url = URL_Head + illust.getId(); - Common.copy(mContext, url); - return true; - } else if (menuItem.getItemId() == R.id.action_show_original) { - baseBind.recyclerView.setAdapter(new IllustAdapter(mActivity, FragmentIllust.this, illust, - recyHeight, true)); - return true; - } else if (menuItem.getItemId() == R.id.action_mute_illust) { - PixivOperate.muteIllust(illust); - return true; - } else if (menuItem.getItemId() == R.id.action_flag_illust) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "举报插画"); - intent.putExtra(FlagDescFragment.FlagObjectIdKey, illust.getId()); - intent.putExtra(FlagDescFragment.FlagObjectTypeKey, ObjectSpec.POST); - startActivity(intent); - return true; - } - return false; - } - }); - if (illust.isIs_bookmarked()) { - baseBind.postLike.setImageResource(R.drawable.ic_favorite_red_24dp); - } else { - baseBind.postLike.setImageResource(R.drawable.ic_favorite_grey_24dp); - } - baseBind.postLike.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (illust.isIs_bookmarked()) { - baseBind.postLike.setImageResource(R.drawable.ic_favorite_grey_24dp); - } else { - baseBind.postLike.setImageResource(R.drawable.ic_favorite_red_24dp); - } - PixivOperate.postLikeDefaultStarType(illust); - } - }); - baseBind.postLike.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(Params.ILLUST_ID, illust.getId()); - intent.putExtra(Params.DATA_TYPE, Params.TYPE_ILLUST); - intent.putExtra(Params.TAG_NAMES, illust.getTagNames()); - intent.putExtra(Params.LAST_CLASS, getClass().getSimpleName()); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "按标签收藏"); - startActivity(intent); - return true; - } - }); - baseBind.illustTag.setAdapter(new TagAdapter(illust.getTags()) { - @Override - public View getView(FlowLayout parent, int position, TagsBean s) { - TextView tv = (TextView) LayoutInflater.from(mContext).inflate(R.layout.recy_single_line_text_new, - parent, false); - String tag = s.getName(); - if (!TextUtils.isEmpty(s.getTranslated_name())) { - tag = tag + "/" + s.getTranslated_name(); - } - tv.setText(tag); - return tv; - } - }); - baseBind.illustTag.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() { - @Override - public boolean onTagClick(View view, int position, FlowLayout parent) { - Intent intent = new Intent(mContext, SearchActivity.class); - intent.putExtra(Params.KEY_WORD, illust.getTags().get(position).getName()); - intent.putExtra(Params.INDEX, 0); - startActivity(intent); - return true; - } - }); - baseBind.illustTag.setOnTagLongClickListener(new TagFlowLayout.OnTagLongClickListener(){ - @Override - public boolean onTagLongClick(View view, int position, FlowLayout parent) { - // 弹出菜单:固定+复制 - String tagName = illust.getTags().get(position).getName(); - SearchEntity searchEntity = PixivOperate.getSearchHistory(tagName, SEARCH_TYPE_DB_KEYWORD); - boolean isPinned = searchEntity != null && searchEntity.isPinned(); - new QMUIDialog.MessageDialogBuilder(mContext) - .setTitle(tagName) - .setSkinManager(QMUISkinManager.defaultInstance(mContext)) - .addAction(isPinned ? getString(R.string.string_443) : getString(R.string.string_442), new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - PixivOperate.insertPinnedSearchHistory(tagName, SEARCH_TYPE_DB_KEYWORD, !isPinned); - Common.showToast(R.string.operate_success); - dialog.dismiss(); - } - }) - .addAction(getString(R.string.string_120), new QMUIDialogAction.ActionListener() { - @Override - public void onClick(QMUIDialog dialog, int index) { - Common.copy(mContext, tagName); - dialog.dismiss(); - } - }) - .create() - .show(); - return true; - } - }); - baseBind.illustSize.setText(getString(R.string.string_193, illust.getWidth(), illust.getHeight())); - baseBind.illustId.setText(getString(R.string.string_194, illust.getId())); - baseBind.userId.setText(getString(R.string.string_195, illust.getUser().getId())); - - final BottomSheetBehavior sheetBehavior = BottomSheetBehavior.from(baseBind.coreLinear); - - baseBind.coreLinear.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - final int realHeight = baseBind.bottomBar.getHeight() + - baseBind.viewDivider.getHeight() + - baseBind.secondLinear.getHeight(); - final int maxHeight = getResources().getDisplayMetrics().heightPixels * 3 / 4; - ViewGroup.LayoutParams params = baseBind.coreLinear.getLayoutParams(); - int slideMaxHeight = Math.min(realHeight, maxHeight); - params.height = slideMaxHeight; - baseBind.coreLinear.setLayoutParams(params); - - final int bottomCardHeight = baseBind.bottomBar.getHeight(); - final int deltaY = slideMaxHeight - baseBind.bottomBar.getHeight(); - sheetBehavior.setPeekHeight(bottomCardHeight, true); - - //设置占位view大小 - ViewGroup.LayoutParams headParams = baseBind.helperView.getLayoutParams(); - headParams.height = bottomCardHeight - DensityUtil.dp2px(16.0f); - baseBind.helperView.setLayoutParams(headParams); - - sheetBehavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { - @Override - public void onStateChanged(@NonNull View bottomSheet, int newState) { - - } - - @Override - public void onSlide(@NonNull View bottomSheet, float slideOffset) { - baseBind.refreshLayout.setTranslationY(-deltaY * slideOffset * 0.7f); - } - }); - - baseBind.recyclerView.setLayoutManager(new LinearLayoutManager(mContext)); - - recyHeight = baseBind.recyclerView.getHeight(); - IllustAdapter adapter = new IllustAdapter(mActivity, FragmentIllust.this, illust, recyHeight, false); - baseBind.recyclerView.setAdapter(adapter); - baseBind.coreLinear.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - }); - - baseBind.related.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "相关作品"); - intent.putExtra(Params.ILLUST_ID, illust.getId()); - intent.putExtra(Params.ILLUST_TITLE, illust.getTitle()); - startActivity(intent); - } - }); - baseBind.comment.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "相关评论"); - intent.putExtra(Params.ILLUST_ID, illust.getId()); - intent.putExtra(Params.ILLUST_TITLE, illust.getTitle()); - startActivity(intent); - } - }); - baseBind.illustLike.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = new Intent(mContext, TemplateActivity.class); - intent.putExtra(Params.CONTENT, illust); - intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "喜欢这个作品的用户"); - startActivity(intent); - } - }); - if (!TextUtils.isEmpty(illust.getCaption())) { - baseBind.description.setVisibility(View.VISIBLE); - baseBind.description.setHtml(illust.getCaption()); - } else { - baseBind.description.setVisibility(View.GONE); - } - View.OnClickListener toUserActivityListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(mContext, UserActivity.class); - intent.putExtra(Params.USER_ID, illust.getUser().getId()); - startActivity(intent); - } - }; - baseBind.relaIllustBrief.setOnClickListener(toUserActivityListener); - baseBind.userName.setOnClickListener(toUserActivityListener); - baseBind.userName.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - Common.copy(mContext, illust.getUser().getName()); - return true; - } - }); - baseBind.follow.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Integer integerValue = Shaft.appViewModel.getFollowUserLiveData(illust.getUser().getId()).getValue(); - if (AppLevelViewModel.FollowUserStatus.isFollowed(integerValue)) { - PixivOperate.postUnFollowUser(illust.getUser().getId()); - illust.getUser().setIs_followed(false); - } else { - PixivOperate.postFollowUser(illust.getUser().getId(), Params.TYPE_PUBLIC); - illust.getUser().setIs_followed(true); - } - } - }); - - baseBind.follow.setOnLongClickListener(v1 -> { - Integer integerValue = Shaft.appViewModel.getFollowUserLiveData(illust.getUser().getId()).getValue(); - if (!AppLevelViewModel.FollowUserStatus.isFollowed(integerValue)) { - illust.getUser().setIs_followed(true); - } - PixivOperate.postFollowUser(illust.getUser().getId(), Params.TYPE_PRIVATE); - return true; - }); - - baseBind.userName.setText(illust.getUser().getName()); - baseBind.postTime.setText(String.format("%s投递", Common.getLocalYYYYMMDDHHMMString(illust.getCreate_date()))); - baseBind.totalView.setText(String.valueOf(illust.getTotal_view())); - baseBind.totalLike.setText(String.valueOf(illust.getTotal_bookmarks())); - baseBind.download.setChangeAlphaWhenPress(true); - baseBind.related.setChangeAlphaWhenPress(true); - baseBind.comment.setChangeAlphaWhenPress(true); - baseBind.download.setOnClickListener(v -> { - if (illust.getPage_count() == 1) { - IllustDownload.downloadIllustFirstPage(illust, (BaseActivity) mContext); - } else { - IllustDownload.downloadIllustAllPages(illust, (BaseActivity) mContext); - } - checkDownload(); - if (Shaft.sSettings.isAutoPostLikeWhenDownload() && !illust.isIs_bookmarked()) { - PixivOperate.postLikeDefaultStarType(illust); - } - }); - - baseBind.download.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - String[] IMG_RESOLUTION_TITLE = new String[]{ - getString(R.string.string_280), - getString(R.string.string_281), - getString(R.string.string_282), - getString(R.string.string_283) - }; - String[] IMG_RESOLUTION = new String[]{ - Params.IMAGE_RESOLUTION_ORIGINAL, - Params.IMAGE_RESOLUTION_LARGE, - Params.IMAGE_RESOLUTION_MEDIUM, - Params.IMAGE_RESOLUTION_SQUARE_MEDIUM - }; - new QMUIDialog.CheckableDialogBuilder(mContext) - .setSkinManager(QMUISkinManager.defaultInstance(mContext)) - .addItems(IMG_RESOLUTION_TITLE, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (illust.getPage_count() == 1) { - IllustDownload.downloadIllustFirstPageWithResolution(illust, IMG_RESOLUTION[which], (BaseActivity) mContext); - } else { - IllustDownload.downloadIllustAllPagesWithResolution(illust, IMG_RESOLUTION[which], (BaseActivity) mContext); - } - dialog.dismiss(); - } - }) - .create() - .show(); - return true; - } - }); - baseBind.illustId.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Common.copy(mContext, String.valueOf(illust.getId())); - } - }); - baseBind.userId.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Common.copy(mContext, String.valueOf(illust.getUser().getId())); - } - }); - Glide.with(mContext) - .load(GlideUtil.getUrl(illust.getUser().getProfile_image_urls().getMedium())) - .error(R.drawable.no_profile) - .into(baseBind.userHead); - - Shaft.appViewModel.getFollowUserLiveData(illust.getUser().getId()).observe(this, new Observer() { - @Override - public void onChanged(Integer integer) { - updateFollowUserUI(integer); - } - }); - } - - private CallBackReceiver mReceiver; - - @Override - public void onResume() { - super.onResume(); - checkDownload(); - } - - private int recyHeight = 0; - - private void checkDownload() { - if (Common.isIllustDownloaded(illust)) { - baseBind.download.setText(R.string.string_337); - } else { - baseBind.download.setText(R.string.string_72); - } - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - IntentFilter intentFilter = new IntentFilter(); - mReceiver = new CallBackReceiver(new BaseReceiver.CallBack() { - @Override - public void onReceive(Context context, Intent intent) { - Bundle bundle = intent.getExtras(); - if (bundle != null) { - int id = bundle.getInt(Params.ID); - if (illust.getId() == id) { - boolean isLiked = bundle.getBoolean(Params.IS_LIKED); - if (isLiked) { - illust.setIs_bookmarked(true); - baseBind.postLike.setImageResource(R.drawable.ic_favorite_red_24dp); - int beforeStarCount = illust.getTotal_bookmarks(); - int afterStarCount = beforeStarCount + 1; - illust.setTotal_bookmarks(afterStarCount); - baseBind.totalLike.setText(String.valueOf(afterStarCount)); - } else { - illust.setIs_bookmarked(false); - baseBind.postLike.setImageResource(R.drawable.ic_favorite_grey_24dp); - int beforeStarCount = illust.getTotal_bookmarks(); - int afterStarCount = beforeStarCount - 1; - illust.setTotal_bookmarks(afterStarCount); - baseBind.totalLike.setText(String.valueOf(afterStarCount)); - } - } - } - } - }); - intentFilter.addAction(Params.LIKED_ILLUST); - LocalBroadcastManager.getInstance(mContext).registerReceiver(mReceiver, intentFilter); - } - - @Override - public void onDestroy() { - if (mReceiver != null) { - LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mReceiver); - } - super.onDestroy(); - } - - @Override - public void onDestroyView() { - try { - if (baseBind != null && baseBind.recyclerView != null) { - baseBind.recyclerView.setAdapter(null); - } - } catch (Exception e) { - e.printStackTrace(); - } - super.onDestroyView(); - } - - @Override - public void vertical() { - //竖屏 - baseBind.toolbar.setPadding(0, Shaft.statusHeight, 0, 0); - } - - @Override - public SmartRefreshLayout getSmartRefreshLayout() { - return baseBind.refreshLayout; - } - - private void updateFollowUserUI(int status){ - if(AppLevelViewModel.FollowUserStatus.isFollowed(status)){ - baseBind.follow.setText(R.string.string_177); - }else{ - baseBind.follow.setText(R.string.string_178); - } - } -} diff --git a/app/src/main/java/ceui/lisa/fragments/FragmentIllust.kt b/app/src/main/java/ceui/lisa/fragments/FragmentIllust.kt new file mode 100644 index 000000000..f82d43a00 --- /dev/null +++ b/app/src/main/java/ceui/lisa/fragments/FragmentIllust.kt @@ -0,0 +1,580 @@ +package ceui.lisa.fragments + +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Handler +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import android.text.TextUtils +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnLongClickListener +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.TextView +import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.recyclerview.widget.LinearLayoutManager +import ceui.lisa.R +import ceui.lisa.activities.* +import ceui.lisa.adapters.IllustAdapter +import ceui.lisa.database.AppDatabase +import ceui.lisa.databinding.FragmentIllustBinding +import ceui.lisa.dialogs.MuteDialog +import ceui.lisa.download.IllustDownload +import ceui.lisa.models.* +import ceui.lisa.notification.CallBackReceiver +import ceui.lisa.utils.* +import ceui.loxia.* +import ceui.refactor.setOnClick +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.qmuiteam.qmui.skin.QMUISkinManager +import com.qmuiteam.qmui.widget.dialog.QMUIDialog.CheckableDialogBuilder +import com.qmuiteam.qmui.widget.dialog.QMUIDialog.MessageDialogBuilder +import com.scwang.smart.refresh.layout.SmartRefreshLayout +import com.zhy.view.flowlayout.FlowLayout +import com.zhy.view.flowlayout.TagAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class FragmentIllust : SwipeFragment() { + + private val safeArgs by threadSafeArgs() + + public override fun initLayout() { + mLayoutID = R.layout.fragment_illust + } + + override fun initView() { + val illustLiveData = ObjectPool.get(safeArgs.illustId.toLong()) + illustLiveData.observe(viewLifecycleOwner) { illust -> + updateIllust(illust) + } + val userId = illustLiveData.value?.user?.id ?: return + val userLiveData = ObjectPool.get(userId.toLong()) + userLiveData.observe(viewLifecycleOwner) { user -> + updateUser(user) + Common.showLog("updateUser invoke ${user.isIs_followed}") + } + val illust = illustLiveData.value ?: return + baseBind.user = userLiveData + viewLifecycleOwner.lifecycleScope.launch { + val dao = AppDatabase.getAppDatabase(requireContext()).searchDao() + val muteIllust = withContext(Dispatchers.IO) { + dao.getIllustMuteEntityByID(illust.id) + } + val muteUser = withContext(Dispatchers.IO) { + dao.getUserMuteEntityByIDLiveData((illust.user?.userId ?: 0)) + } + combineLatest(muteIllust, muteUser).observe(viewLifecycleOwner) { + val illustEntity = it.first + val userEntity = it.second + if (illustEntity == null && userEntity == null) { + baseBind.contentFrame.isVisible = true + baseBind.abandonedFrame.isVisible = false + } else { + baseBind.contentFrame.isVisible = false + baseBind.abandonedFrame.isVisible = true + baseBind.cancelMuteIllust.isVisible = illustEntity != null + baseBind.cancelMuteUser.isVisible = userEntity != null + + if (illustEntity != null) { + baseBind.cancelMuteIllust.setOnClick { + viewLifecycleOwner.lifecycleScope.launch { + it.showProgress() + delay(600L) + dao.deleteMuteEntity(illustEntity) + it.hideProgress() + } + } + } + if (userEntity != null) { + baseBind.cancelMuteUser.setOnClick { + viewLifecycleOwner.lifecycleScope.launch { + it.showProgress() + delay(600L) + dao.deleteMuteEntity(userEntity) + it.hideProgress() + } + } + } + } + } + } + } + + private fun updateUser(user: UserBean) { + if (user.isIs_followed) { + baseBind.follow.isVisible = false + baseBind.unfollow.isVisible = true + baseBind.unfollow.setOnClick { + unfollowUser(it, user.id) + } + } else { + baseBind.unfollow.isVisible = false + baseBind.follow.isVisible = true + baseBind.follow.setOnClick { + followUser(it, user.id, Params.TYPE_PUBLIC) + } + baseBind.follow.setOnLongClickListener { + followUser((it as ProgressTextButton), user.id, Params.TYPE_PRIVATE) + true + } + } + baseBind.relaIllustBrief.setOnClick { + val intent = Intent(mContext, UserActivity::class.java) + intent.putExtra(Params.USER_ID, user.id) + startActivity(intent) + } + baseBind.userName.setOnClick { + val intent = Intent(mContext, UserActivity::class.java) + intent.putExtra(Params.USER_ID, user.id) + startActivity(intent) + } + baseBind.userName.setOnLongClickListener { + Common.copy(mContext, user.name) + true + } + + baseBind.userName.text = user.name + } + + private fun updateIllust(illust: IllustsBean) { + if (illust.id == 0 || !illust.isVisible) { + Common.showToast(R.string.string_206) + Handler().postDelayed({ finish() }, 1000) + return + } + + baseBind.leave.setOnClick { + viewLifecycleOwner.lifecycleScope.launch { + it.showProgress() + delay(600L) + requireActivity().finish() + it.hideProgress() + } + } + + if (illust.series != null && !TextUtils.isEmpty(illust.series.title)) { + val clickableSpan: ClickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "漫画系列详情") + intent.putExtra(Params.MANGA_SERIES_ID, illust.series.id) + startActivity(intent) + } + + override fun updateDrawState(ds: TextPaint) { + ds.color = Common.resolveThemeAttribute(mContext, R.attr.colorPrimary) + } + } + val spannableString: SpannableString + val seriesString = getString(R.string.string_229) + spannableString = SpannableString( + String.format( + "@%s %s", + seriesString, illust.title + ) + ) + spannableString.setSpan( + clickableSpan, 0, seriesString.length + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + baseBind.title.movementMethod = LinkMovementMethod.getInstance() + baseBind.title.text = spannableString + } else { + baseBind.title.text = illust.title + } + baseBind.title.setOnLongClickListener { + Common.copy(mContext, illust.title) + true + } + baseBind.toolbar.inflateMenu(R.menu.share) + baseBind.toolbar.setNavigationOnClickListener { v: View? -> mActivity.finish() } + baseBind.toolbar.setOnMenuItemClickListener(Toolbar.OnMenuItemClickListener { menuItem -> + if (menuItem.itemId == R.id.action_share) { + object : ShareIllust(mContext, illust) { + override fun onPrepare() {} + }.execute() + return@OnMenuItemClickListener true + } else if (menuItem.itemId == R.id.action_share_image) { + Glide.with(mContext) + .asBitmap() + .load( + GlideUrlChild( + IllustDownload.getUrl( + illust, + 0, + Params.IMAGE_RESOLUTION_LARGE + ) + ) + ) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + return false + } + + override fun onResourceReady( + resource: Bitmap?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + val uri = Common.copyBitmapToImageCacheFolder( + resource, + illust.id.toString() + ".png" + ) + if (uri != null) { + val shareIntent = Intent() + shareIntent.action = Intent.ACTION_SEND + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + shareIntent.setDataAndType( + uri, + mContext.contentResolver.getType(uri) + ) + shareIntent.putExtra(Intent.EXTRA_STREAM, uri) + startActivity( + Intent.createChooser( + shareIntent, + getString(R.string.share) + ) + ) + } + return true + } + }).submit() + } else if (menuItem.itemId == R.id.action_dislike) { + val muteDialog = MuteDialog.newInstance(illust) + muteDialog.show(childFragmentManager, "MuteDialog") + return@OnMenuItemClickListener true + } else if (menuItem.itemId == R.id.action_copy_link) { + val url = ShareIllust.URL_Head + illust.id + Common.copy(mContext, url) + return@OnMenuItemClickListener true + } else if (menuItem.itemId == R.id.action_show_original) { + baseBind.recyclerView.adapter = IllustAdapter( + mActivity, this@FragmentIllust, illust, + recyHeight, true + ) + return@OnMenuItemClickListener true + } else if (menuItem.itemId == R.id.action_mute_illust) { + PixivOperate.muteIllust(illust) + return@OnMenuItemClickListener true + } else if (menuItem.itemId == R.id.action_flag_illust) { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "举报插画") + intent.putExtra(FlagDescFragment.FlagObjectIdKey, illust.id) + intent.putExtra(FlagDescFragment.FlagObjectTypeKey, ObjectSpec.POST) + startActivity(intent) + return@OnMenuItemClickListener true + } + false + }) + if (illust.isIs_bookmarked) { + baseBind.postLike.setImageResource(R.drawable.ic_favorite_red_24dp) + } else { + baseBind.postLike.setImageResource(R.drawable.ic_favorite_grey_24dp) + } + baseBind.postLike.setOnClick { + if (illust.isIs_bookmarked) { + baseBind.postLike.setImageResource(R.drawable.ic_favorite_grey_24dp) + } else { + baseBind.postLike.setImageResource(R.drawable.ic_favorite_red_24dp) + } + PixivOperate.postLikeDefaultStarType(illust) + } + baseBind.postLike.setOnLongClickListener(object : OnLongClickListener { + override fun onLongClick(v: View): Boolean { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(Params.ILLUST_ID, illust.id) + intent.putExtra(Params.DATA_TYPE, Params.TYPE_ILLUST) + intent.putExtra(Params.TAG_NAMES, illust.tagNames) + intent.putExtra(Params.LAST_CLASS, javaClass.simpleName) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "按标签收藏") + startActivity(intent) + return true + } + }) + baseBind.illustTag.adapter = object : TagAdapter( + illust.tags + ) { + override fun getView(parent: FlowLayout, position: Int, s: TagsBean): View { + val tv = LayoutInflater.from(mContext).inflate( + R.layout.recy_single_line_text_new, + parent, false + ) as TextView + var tag = s.name + if (!TextUtils.isEmpty(s.translated_name)) { + tag = tag + "/" + s.translated_name + } + tv.text = tag + return tv + } + } + baseBind.illustTag.setOnTagClickListener { view, position, parent -> + val intent = Intent(mContext, SearchActivity::class.java) + intent.putExtra(Params.KEY_WORD, illust.tags[position].name) + intent.putExtra(Params.INDEX, 0) + startActivity(intent) + true + } + baseBind.illustTag.setOnTagLongClickListener { view, position, parent -> // 弹出菜单:固定+复制 + val tagName = illust.tags[position].name + val searchEntity = + PixivOperate.getSearchHistory(tagName, SearchTypeUtil.SEARCH_TYPE_DB_KEYWORD) + val isPinned = searchEntity != null && searchEntity.isPinned + MessageDialogBuilder(mContext) + .setTitle(tagName) + .setSkinManager(QMUISkinManager.defaultInstance(mContext)) + .addAction(if (isPinned) getString(R.string.string_443) else getString(R.string.string_442)) { dialog, index -> + PixivOperate.insertPinnedSearchHistory( + tagName, + SearchTypeUtil.SEARCH_TYPE_DB_KEYWORD, + !isPinned + ) + Common.showToast(R.string.operate_success) + dialog.dismiss() + } + .addAction(getString(R.string.string_120)) { dialog, index -> + Common.copy(mContext, tagName) + dialog.dismiss() + } + .create() + .show() + true + } + baseBind.illustSize.text = getString(R.string.string_193, illust.width, illust.height) + baseBind.illustId.text = getString(R.string.string_194, illust.id) + baseBind.userId.text = getString(R.string.string_195, illust.user.id) + val sheetBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from( + baseBind.coreLinear + ) + baseBind.coreLinear.viewTreeObserver.addOnGlobalLayoutListener(object : + OnGlobalLayoutListener { + override fun onGlobalLayout() { + val realHeight = baseBind.bottomBar.height + + baseBind.viewDivider.height + + baseBind.secondLinear.height + val maxHeight = resources.displayMetrics.heightPixels * 3 / 4 + val params = baseBind.coreLinear.layoutParams + val slideMaxHeight = Math.min(realHeight, maxHeight) + params.height = slideMaxHeight + baseBind.coreLinear.layoutParams = params + val bottomCardHeight = baseBind.bottomBar.height + val deltaY = slideMaxHeight - baseBind.bottomBar.height + sheetBehavior.setPeekHeight(bottomCardHeight, true) + + //设置占位view大小 + val headParams = baseBind.helperView.layoutParams + headParams.height = bottomCardHeight - DensityUtil.dp2px(16.0f) + baseBind.helperView.layoutParams = headParams + sheetBehavior.addBottomSheetCallback(object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) {} + override fun onSlide(bottomSheet: View, slideOffset: Float) { + baseBind.refreshLayout.translationY = -deltaY * slideOffset * 0.7f + } + }) + baseBind.recyclerView.layoutManager = LinearLayoutManager(mContext) + recyHeight = baseBind.recyclerView.height + val adapter = + IllustAdapter(mActivity, this@FragmentIllust, illust, recyHeight, false) + baseBind.recyclerView.adapter = adapter + baseBind.coreLinear.viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }) + baseBind.related.setOnClick { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "相关作品") + intent.putExtra(Params.ILLUST_ID, illust.id) + intent.putExtra(Params.ILLUST_TITLE, illust.title) + startActivity(intent) + } + baseBind.comment.setOnClick { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "相关评论") + intent.putExtra(Params.ILLUST_ID, illust.id) + intent.putExtra(Params.ILLUST_TITLE, illust.title) + startActivity(intent) + } + baseBind.illustLike.setOnClick { + val intent = Intent(mContext, TemplateActivity::class.java) + intent.putExtra(Params.CONTENT, illust) + intent.putExtra(TemplateActivity.EXTRA_FRAGMENT, "喜欢这个作品的用户") + startActivity(intent) + } + if (!TextUtils.isEmpty(illust.caption)) { + baseBind.description.visibility = View.VISIBLE + baseBind.description.setHtml(illust.caption) + } else { + baseBind.description.visibility = View.GONE + } + baseBind.postTime.text = String.format( + "%s投递", Common.getLocalYYYYMMDDHHMMString( + illust.create_date + ) + ) + baseBind.totalView.text = illust.total_view.toString() + baseBind.totalLike.text = illust.total_bookmarks.toString() + baseBind.download.setChangeAlphaWhenPress(true) + baseBind.related.setChangeAlphaWhenPress(true) + baseBind.comment.setChangeAlphaWhenPress(true) + baseBind.download.setOnClick { v: View? -> + if (illust.page_count == 1) { + IllustDownload.downloadIllustFirstPage(illust, mContext as BaseActivity<*>) + } else { + IllustDownload.downloadIllustAllPages(illust, mContext as BaseActivity<*>) + } + checkDownload() + if (Shaft.sSettings.isAutoPostLikeWhenDownload && !illust.isIs_bookmarked) { + PixivOperate.postLikeDefaultStarType(illust) + } + } + baseBind.download.setOnLongClickListener { + val IMG_RESOLUTION_TITLE = arrayOf( + getString(R.string.string_280), + getString(R.string.string_281), + getString(R.string.string_282), + getString(R.string.string_283) + ) + val IMG_RESOLUTION = arrayOf( + Params.IMAGE_RESOLUTION_ORIGINAL, + Params.IMAGE_RESOLUTION_LARGE, + Params.IMAGE_RESOLUTION_MEDIUM, + Params.IMAGE_RESOLUTION_SQUARE_MEDIUM + ) + CheckableDialogBuilder(mContext) + .setSkinManager(QMUISkinManager.defaultInstance(mContext)) + .addItems(IMG_RESOLUTION_TITLE) { dialog, which -> + if (illust.page_count == 1) { + IllustDownload.downloadIllustFirstPageWithResolution( + illust, + IMG_RESOLUTION[which], + mContext as BaseActivity<*> + ) + } else { + IllustDownload.downloadIllustAllPagesWithResolution( + illust, + IMG_RESOLUTION[which], + mContext as BaseActivity<*> + ) + } + dialog.dismiss() + } + .create() + .show() + true + } + baseBind.illustId.setOnClick { Common.copy(mContext, illust.id.toString()) } + baseBind.userId.setOnClick { Common.copy(mContext, illust.user.id.toString()) } + Glide.with(mContext) + .load(GlideUtil.getUrl(illust.user?.profile_image_urls?.medium)) + .error(R.drawable.no_profile) + .into(baseBind.userHead) + } + + private var mReceiver: CallBackReceiver? = null + override fun onResume() { + super.onResume() + checkDownload() + } + + private var recyHeight = 0 + private fun checkDownload() { + val illust = ObjectPool.get(safeArgs.illustId.toLong()).value ?: return + if (Common.isIllustDownloaded(illust)) { + baseBind.download.setText(R.string.string_337) + } else { + baseBind.download.setText(R.string.string_72) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val intentFilter = IntentFilter() + val illust = ObjectPool.get(safeArgs.illustId.toLong()).value ?: return + mReceiver = CallBackReceiver { context, intent -> + val bundle = intent.extras + if (bundle != null) { + val id = bundle.getInt(Params.ID) + if (illust.id == id) { + val isLiked = bundle.getBoolean(Params.IS_LIKED) + if (isLiked) { + illust.isIs_bookmarked = true + baseBind.postLike.setImageResource(R.drawable.ic_favorite_red_24dp) + val beforeStarCount = illust.total_bookmarks + val afterStarCount = beforeStarCount + 1 + illust.total_bookmarks = afterStarCount + baseBind.totalLike.text = afterStarCount.toString() + } else { + illust.isIs_bookmarked = false + baseBind.postLike.setImageResource(R.drawable.ic_favorite_grey_24dp) + val beforeStarCount = illust.total_bookmarks + val afterStarCount = beforeStarCount - 1 + illust.total_bookmarks = afterStarCount + baseBind.totalLike.text = afterStarCount.toString() + } + } + } + } + intentFilter.addAction(Params.LIKED_ILLUST) + mReceiver?.let { + LocalBroadcastManager.getInstance(mContext).registerReceiver(it, intentFilter) + } + } + + override fun onDestroy() { + mReceiver?.let { + LocalBroadcastManager.getInstance(mContext).unregisterReceiver(it) + } + super.onDestroy() + } + + override fun onDestroyView() { + try { + baseBind.recyclerView.adapter = null + } catch (e: Exception) { + e.printStackTrace() + } + super.onDestroyView() + } + + override fun vertical() { + //竖屏 + baseBind.toolbar.setPadding(0, Shaft.statusHeight, 0, 0) + } + + override fun getSmartRefreshLayout(): SmartRefreshLayout { + return baseBind.refreshLayout + } + + companion object { + @JvmStatic + fun newInstance(illustId: Int): FragmentIllust { + return FragmentIllust().apply { + arguments = Bundle().apply { + putInt("illust_id", illustId) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ceui/lisa/fragments/FragmentMutedObjects.java b/app/src/main/java/ceui/lisa/fragments/FragmentMutedObjects.java index 62c53f794..d6fb43794 100644 --- a/app/src/main/java/ceui/lisa/fragments/FragmentMutedObjects.java +++ b/app/src/main/java/ceui/lisa/fragments/FragmentMutedObjects.java @@ -61,7 +61,7 @@ public void onClick(QMUIDialog dialog, int index) { new QMUIDialogAction.ActionListener() { @Override public void onClick(QMUIDialog dialog, int index) { - AppDatabase.getAppDatabase(mContext).downloadDao().deleteMuteEntity(allItems.get(position)); + AppDatabase.getAppDatabase(mContext).searchDao().deleteMuteEntity(allItems.get(position)); allItems.remove(position); mAdapter.notifyItemRemoved(position); mAdapter.notifyItemRangeChanged(position, allItems.size() - position); diff --git a/app/src/main/java/ceui/lisa/fragments/FragmentRecmdIllust.java b/app/src/main/java/ceui/lisa/fragments/FragmentRecmdIllust.java index 756adcf79..6fd61b1db 100644 --- a/app/src/main/java/ceui/lisa/fragments/FragmentRecmdIllust.java +++ b/app/src/main/java/ceui/lisa/fragments/FragmentRecmdIllust.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import ceui.lisa.R; import ceui.lisa.activities.Shaft; @@ -42,6 +43,7 @@ import ceui.lisa.view.SpacesItemWithHeadDecoration; import ceui.lisa.viewmodel.BaseModel; import ceui.lisa.viewmodel.RecmdModel; +import ceui.loxia.ObjectPool; public class FragmentRecmdIllust extends NetListFragment { @@ -213,6 +215,12 @@ public List execute() throws Exception { }, new NullCtrl>() { @Override public void success(List illustsBeans) { + illustsBeans.forEach(new Consumer() { + @Override + public void accept(IllustsBean illustsBean) { + ObjectPool.INSTANCE.updateIllust(illustsBean); + } + }); allItems.addAll(illustsBeans); ((RecmdModel) mModel).getRankList().addAll(illustsBeans); mModel.tidyAppViewModel(illustsBeans); diff --git a/app/src/main/java/ceui/lisa/helper/IllustNovelFilter.java b/app/src/main/java/ceui/lisa/helper/IllustNovelFilter.java index 19e6bf14e..7147a4383 100644 --- a/app/src/main/java/ceui/lisa/helper/IllustNovelFilter.java +++ b/app/src/main/java/ceui/lisa/helper/IllustNovelFilter.java @@ -55,14 +55,14 @@ public static boolean judgeID(NovelBean illust) { public static boolean judgeUserID(IllustsBean illust) { MuteEntity temp = AppDatabase.getAppDatabase(Shaft.getContext()) .searchDao() - .getMuteEntityByID(illust.getUser().getUserId()); + .getUserMuteEntityByID(illust.getUser().getUserId()); return temp != null; } public static boolean judgeUserID(NovelBean illust) { MuteEntity temp = AppDatabase.getAppDatabase(Shaft.getContext()) .searchDao() - .getMuteEntityByID(illust.getUser().getUserId()); + .getUserMuteEntityByID(illust.getUser().getUserId()); return temp != null; } diff --git a/app/src/main/java/ceui/lisa/utils/PixivOperate.java b/app/src/main/java/ceui/lisa/utils/PixivOperate.java index dc0a86563..b98ab4562 100644 --- a/app/src/main/java/ceui/lisa/utils/PixivOperate.java +++ b/app/src/main/java/ceui/lisa/utils/PixivOperate.java @@ -64,6 +64,7 @@ import ceui.lisa.models.UserModel; import ceui.lisa.models.IllustsBean; import ceui.lisa.viewmodel.AppLevelViewModel; +import ceui.loxia.ObjectPool; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import retrofit2.Call; @@ -104,6 +105,7 @@ public void next(NullResponse nullResponse) { intent.putExtra(Params.IS_LIKED, true); LocalBroadcastManager.getInstance(Shaft.getContext()).sendBroadcast(intent); + ObjectPool.INSTANCE.followUser(userID); if (followType.equals(Params.TYPE_PUBLIC)) { Shaft.appViewModel.updateFollowUserStatus(userID, AppLevelViewModel.FollowUserStatus.FOLLOWED_PUBLIC); Common.showToast(getString(R.string.like_success_public)); @@ -128,7 +130,7 @@ public void next(NullResponse nullResponse) { intent.putExtra(Params.IS_LIKED, false); LocalBroadcastManager.getInstance(Shaft.getContext()).sendBroadcast(intent); Shaft.appViewModel.updateFollowUserStatus(userID, AppLevelViewModel.FollowUserStatus.NOT_FOLLOW); - + ObjectPool.INSTANCE.unFollowUser(userID); Common.showToast(getString(R.string.cancel_like)); } }); diff --git a/app/src/main/java/ceui/loxia/API.kt b/app/src/main/java/ceui/loxia/API.kt index db2443cfd..d89aabde1 100644 --- a/app/src/main/java/ceui/loxia/API.kt +++ b/app/src/main/java/ceui/loxia/API.kt @@ -14,4 +14,17 @@ interface API { @Field("type_of_problem") type_of_problem: String?, @Field("message") message: String? ): NullResponse + + @FormUrlEncoded + @POST("/v1/user/follow/add") + suspend fun postFollow( + @Field("user_id") user_id: Long, + @Field("restrict") followType: String + ) + + @FormUrlEncoded + @POST("/v1/user/follow/delete") + suspend fun postUnFollow( + @Field("user_id") user_id: Long + ) } \ No newline at end of file diff --git a/app/src/main/java/ceui/loxia/DateParse.kt b/app/src/main/java/ceui/loxia/DateParse.kt new file mode 100644 index 000000000..f2dc38b5d --- /dev/null +++ b/app/src/main/java/ceui/loxia/DateParse.kt @@ -0,0 +1,18 @@ +package ceui.loxia + +object DateParse { + + private const val FAKE_DATE = "2022-03-21 08:24" + + fun displayCreateDate(create_date: String?): String { + if (create_date?.isNotEmpty() == true) { + return if (create_date.contains("T") && create_date.length == 25) { + val str = create_date.substring(0, 16) + str.replace("T", " ") + } else { + FAKE_DATE + } + } + return FAKE_DATE + } +} \ No newline at end of file diff --git a/app/src/main/java/ceui/loxia/LiveDataUtil.kt b/app/src/main/java/ceui/loxia/LiveDataUtil.kt new file mode 100644 index 000000000..5421ef480 --- /dev/null +++ b/app/src/main/java/ceui/loxia/LiveDataUtil.kt @@ -0,0 +1,521 @@ +package ceui.loxia + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch + + +data class Tuple4(val first: T1, val second: T2, val third: T3, val forth: T4) +data class Tuple5(val first: T1, val second: T2, val third: T3, val forth: T4, val fifth: T5) +data class Tuple6(val first: T1, val second: T2, val third: T3, val forth: T4, val fifth: T5, val sixth: T6) + + +fun combineLatest2( + source1: LiveData, + source2: LiveData, + combine: (data1: T1?, data2: T2?) -> S, +) : LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() + + var data1: T1? = source1.value + var data2: T2? = source2.value + + finalLiveData.addSource(source1) { + data1 = it + finalLiveData.value = combine(data1, data2) + } + finalLiveData.addSource(source2) { + data2 = it + finalLiveData.value = combine(data1, data2) + } + + return finalLiveData +} + +fun combineLatest( + source1: LiveData, + source2: LiveData, +) : LiveData> { + return combineLatest2(source1, source2, ::Pair) +} + +fun combineLatest3( + source1: LiveData, + source2: LiveData, + source3: LiveData, + combine: (data1: T1?, data2: T2?, data3: T3?) -> S, +) : LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() + + var data1: T1? = source1.value + var data2: T2? = source2.value + var data3: T3? = source3.value + + finalLiveData.addSource(source1) { + data1 = it + finalLiveData.value = combine(data1, data2, data3) + } + finalLiveData.addSource(source2) { + data2 = it + finalLiveData.value = combine(data1, data2, data3) + } + finalLiveData.addSource(source3) { + data3 = it + finalLiveData.value = combine(data1, data2, data3) + } + + return finalLiveData +} + +fun combineLatest( + source1: LiveData, + source2: LiveData, + source3: LiveData, +) : LiveData> { + return combineLatest3(source1, source2, source3, ::Triple) +} + +fun combineLatest4( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + combine: (data1: T1?, data2: T2?, data3: T3?, data4: T4?) -> S, +) : LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() + + var data1: T1? = source1.value + var data2: T2? = source2.value + var data3: T3? = source3.value + var data4: T4? = source4.value + + finalLiveData.addSource(source1) { + data1 = it + finalLiveData.value = combine(data1, data2, data3, data4) + } + finalLiveData.addSource(source2) { + data2 = it + finalLiveData.value = combine(data1, data2, data3, data4) + } + finalLiveData.addSource(source3) { + data3 = it + finalLiveData.value = combine(data1, data2, data3, data4) + } + + finalLiveData.addSource(source4) { + data4 = it + finalLiveData.value = combine(data1, data2, data3, data4) + } + + return finalLiveData +} + + +fun combineLatest( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, +) : LiveData> { + return combineLatest4(source1, source2, source3, source4, ::Tuple4) +} + + +fun combineLatest5( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, + combine: (data1: T1?, data2: T2?, data3: T3?, data4: T4?, data5: T5?) -> S, +) : LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() + + var data1: T1? = source1.value + var data2: T2? = source2.value + var data3: T3? = source3.value + var data4: T4? = source4.value + var data5: T5? = source5.value + + finalLiveData.addSource(source1) { + data1 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5) + } + finalLiveData.addSource(source2) { + data2 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5) + } + finalLiveData.addSource(source3) { + data3 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5) + } + + finalLiveData.addSource(source4) { + data4 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5) + } + + finalLiveData.addSource(source5) { + data5 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5) + } + + return finalLiveData +} + +fun combineLatest6( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, + source6: LiveData, + combine: (data1: T1?, data2: T2?, data3: T3?, data4: T4?, data5: T5?, data6: T6?) -> S, +) : LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() + + var data1: T1? = source1.value + var data2: T2? = source2.value + var data3: T3? = source3.value + var data4: T4? = source4.value + var data5: T5? = source5.value + var data6: T6? = source6.value + + finalLiveData.addSource(source1) { + data1 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5, data6) + } + finalLiveData.addSource(source2) { + data2 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5, data6) + } + finalLiveData.addSource(source3) { + data3 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5, data6) + } + + finalLiveData.addSource(source4) { + data4 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5, data6) + } + + finalLiveData.addSource(source5) { + data5 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5, data6) + } + + finalLiveData.addSource(source6) { + data6 = it + finalLiveData.value = combine(data1, data2, data3, data4, data5, data6) + } + + return finalLiveData +} + + +fun combineLatest( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, +) : LiveData> { + return combineLatest5(source1, source2, source3, source4, source5, ::Tuple5) +} + +fun combineLatest( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, + source6: LiveData, +) : LiveData> { + return combineLatest6(source1, source2, source3, source4, source5, source6, ::Tuple6) +} + + + +fun MutableLiveData.asLiveData() = this as LiveData + +fun LiveData.timeout(mills: Long): LiveData { + + var hasEmitted = false + + val result = MediatorLiveData() + result.addSource(this) { v -> + if (!hasEmitted) { + result.value = v + hasEmitted = true + } + } + + MainScope().launch { + delay(mills) + if (!hasEmitted) { + result.value = null + hasEmitted = true + } + } + + return result +} + +fun LiveData.waitAtLeast(mills: Long): LiveData { + val start = System.currentTimeMillis() + + val result = MediatorLiveData() + result.addSource(this) { v -> + val wait = mills - (System.currentTimeMillis() - start) + if (wait > 0) { + MainScope().launch { + delay(wait) + result.value = v + } + } else { + result.value = v + } + } + + return result +} + +fun LiveData.delay(mills: Long): LiveData { + val result = MediatorLiveData() + result.addSource(this) { v -> + MainScope().launch { + delay(mills) + result.value = v + } + } + + return result +} + + +fun LiveData.filter(func: (T) -> Boolean): LiveData { + val result = MediatorLiveData() + result.addSource(this) { if (func(it)) result.value = it } + return result +} + + +// deprecated +@Deprecated("Deprecated function", replaceWith = ReplaceWith("LiveData.once")) +fun LiveData.onceDeprecated(): LiveData { + val result = MediatorLiveData() + var hasEmitted = false + result.addSource(this) { + if (!hasEmitted) { + result.value = it + hasEmitted = true + } + } + + return result +} + +fun LiveData.once(): LiveData { + val result = MediatorLiveData() + result.addSource(this) { + result.value = it + result.removeSource(this) + } + + return result +} + + +fun LiveData.notNull(): LiveData { + val result = MediatorLiveData() + result.addSource(this) { + if (it != null) { + result.value = it + result.removeSource(this) + } + } + + return result +} + +fun LiveData.filterNull(): LiveData { + val result = MediatorLiveData() + result.addSource(this) { + if (it != null) { + result.value = it + } + } + + return result +} + +fun LiveData.toDiff(): LiveData> { + val result = MediatorLiveData>() + var oldValue = value + result.addSource(this) { + result.value = Pair(oldValue, it) + oldValue = it + } + + return result +} + + +fun LiveData.debounce(duration: Long) = MediatorLiveData().also { mld -> + val source = this + val handler = Handler(Looper.getMainLooper()) + + val runnable = Runnable { + mld.value = source.value + } + + mld.addSource(source) { + handler.removeCallbacks(runnable) + handler.postDelayed(runnable, duration) + } +} + +fun LiveData.throttle(duration: Long) = MediatorLiveData().also { mld -> + val source = this + val handler = Handler(Looper.getMainLooper()) + + var posted = false + + val runnable = object: Runnable { + override fun run() { + mld.value = source.value + handler.removeCallbacks(this) + posted = false + } + } + + mld.addSource(source) { + if (!posted) { + handler.postDelayed(runnable, duration) + } + } +} + + +suspend fun LiveData.waitForTrue(lifecycleOwner: LifecycleOwner) { + val task = CompletableDeferred() + if (value == true) { + task.complete(Unit) + return task.await() + } + + observe(lifecycleOwner) { + if (it) { + if (!task.isCompleted) { + task.complete(Unit) + } + } + } + + return task.await() +} + +suspend fun LiveData.waitForValue(lifecycleOwner: LifecycleOwner): T { + val task = CompletableDeferred() + + observe(lifecycleOwner) { + if (!task.isCompleted) { + task.complete(it) + } + } + + return task.await() +} + + +data class Observation( + private val liveData: LiveData, + private val observer: Observer, +) { + fun unobserve() { + liveData.removeObserver(observer) + } +} + +fun LiveData.trackedObserve(lifecycleOwner: LifecycleOwner, observer: Observer): Observation { + observe(lifecycleOwner, observer) + return Observation(this, observer) +} + +inline fun LiveData.trackedObserveForEver(crossinline onChanged: (T) -> Unit): Observation { + val observer = Observer { t -> onChanged(t) } + observeForever(observer) + return Observation(this, observer) +} + + +fun MutableLiveData.flipBoolean() { + value = !(this.value ?: false) +} + +fun LiveData.ensureNotNull(listener: (T)->Unit) { + val v = value + if (v != null) { + listener(v) + } else { + observeForever(object : Observer { + var hasTriggered = false + override fun onChanged(t: T) { + if (t != null) { + if (!hasTriggered) { + hasTriggered = true + listener(t) + } + } + } + }) + } +} + +fun LiveData.requireValue(): T { + return value!! +} + +/** + * @param com 如果返回true,代表发生改变 + */ +fun LiveData.distinctUntilChangedBy(com: (X, X) -> Boolean): LiveData { + val outputLiveData: MediatorLiveData = MediatorLiveData() + outputLiveData.addSource(this, object : Observer { + var mFirstTime = true + override fun onChanged(currentValue: X?) { + val previousValue: X? = outputLiveData.value + if (mFirstTime + || previousValue == null && currentValue != null + || previousValue != null && previousValue != currentValue + || previousValue != null && currentValue != null && com(previousValue, currentValue) + ) { + mFirstTime = false + outputLiveData.value = currentValue + } + } + }) + return outputLiveData.asLiveData() +} + +infix fun Pair.to(that: C) = Triple(this.first, this.second, that) + +class UpwardLiveData : MutableLiveData() { + override fun setValue(value: Int?) { + val local = this.value + if (value != null && (local == null || value > local)) { + super.setValue(value) + } + } +} + +fun LiveData.safeObserver(lifecycleOwner: LifecycleOwner, observer: Observer) { + observe(lifecycleOwner, observer) +} \ No newline at end of file diff --git a/app/src/main/java/ceui/loxia/Models.kt b/app/src/main/java/ceui/loxia/Models.kt new file mode 100644 index 000000000..fabcd6ec9 --- /dev/null +++ b/app/src/main/java/ceui/loxia/Models.kt @@ -0,0 +1,595 @@ +package ceui.loxia + +import android.text.TextUtils +import ceui.lisa.interfaces.ListShow +import ceui.lisa.models.ModelObject +import ceui.lisa.models.ObjectSpec +import java.io.Serializable + + + + +data class AccountResponse( + val access_token: String? = null, + val expires_in: Int? = null, + val refresh_token: String? = null, + val scope: String? = null, + val token_type: String? = null, + val user: User? = null +) : Serializable + +data class IllustResponse( + val illusts: List = listOf(), + val next_url: String? = null +) : Serializable, KListShow { + override val displayList: List get() = illusts + override val nextPageUrl: String? get() = next_url +} + +object ObjectType { + const val ILLUST = "illust" + const val MANGA = "manga" + const val GIF = "ugoira" + const val NOVEL = "novel" +} + +object ConstantUser { + const val pixiv = 11L //pixiv事務局 + const val pxv_sensei = 17391869L //pixiv描き方-sensei + const val mangapixiv = 14792128L //MANGA pixiv + const val pixivision = 12848282L //pixivision + const val pxv_sketch = 15241365L //pixiv Sketch + const val pixiv3 = 1085317L // pixiv MARKET事務局 + const val fanbox = 20390859L // pixivFANBOX公式 + + val officialUsers = listOf( + pixiv, + pxv_sensei, + mangapixiv, + pixivision, + pxv_sketch, + pixiv3, + fanbox, + ) +} + + + +data class Illust( + val caption: String? = null, + val create_date: String? = null, + val height: Int = 0, + val id: Long, + val image_urls: ImageUrls? = null, + val is_bookmarked: Boolean? = null, + val is_muted: Boolean? = null, + val meta_pages: List? = null, + val meta_single_page: MetaSinglePage? = null, + val page_count: Int = 0, + val restrict: Int? = null, + val sanity_level: Int? = null, + val series: Any? = null, + val tags: List? = null, + val title: String? = null, + val tools: List? = null, + val total_bookmarks: Int? = null, + val total_view: Int? = null, + val type: String? = null, + val user: User? = null, + val visible: Boolean? = null, + val width: Int = 0, + val x_restrict: Int? = null, +) : Serializable, ModelObject { + override val objectUniqueId: Long + get() = id + override val objectType: Int + get() = ObjectSpec.POST + + fun displayCreateDate(): String { + return DateParse.displayCreateDate(create_date) + } + + fun isGif(): Boolean { + return TextUtils.equals(type, ObjectType.GIF) + } + + fun isManga(): Boolean { + return TextUtils.equals(type, ObjectType.MANGA) + } + + fun isDisabled(): Boolean { + return user?.id == 0L + } + + fun maxUrl(): String? { + if (page_count > 0) { + if (page_count == 1) { + return meta_single_page?.original_image_url + } else { + return meta_pages?.getOrNull(0)?.image_urls?.original + } + } else { + return null + } + } +} + +data class MetaPage( + val image_urls: ImageUrls? = null +) : Serializable + +data class MetaSinglePage( + val original_image_url: String? = null +) : Serializable + +data class Tag( + val name: String? = null, + val translated_name: String? = null +) : Serializable + +object UserGender { + + const val UNKNOWN = 0 + const val MALE = 1 + const val FEMALE = 2 + + fun random(): Int { + return listOf(UNKNOWN, MALE, FEMALE).random() + } +} + +data class User( + val account: String? = null, + val id: Long = 0L, + val is_followed: Boolean? = null, + val name: String? = null, + val profile_image_urls: ImageUrls? = null, + val is_mail_authorized: Boolean? = null, + val is_premium: Boolean? = null, + val mail_address: String? = null, + val gender: Int = UserGender.MALE, + val require_policy_agreement: Boolean? = null, + val x_restrict: Int? = null, + val comment: String? = null, +) : Serializable, ModelObject { + override val objectUniqueId: Long + get() = id + override val objectType: Int + get() = ObjectSpec.USER + + fun isOfficial(): Boolean { + return ConstantUser.officialUsers.contains(id) + } + + fun isPremium(): Boolean { + return is_premium == true + } + + fun hasGender(): Boolean { + return gender != UserGender.UNKNOWN + } +} + +data class ImageUrls( + val large: String? = null, + val medium: String? = null, + val original: String? = null, + val square_medium: String? = null, + val px_16x16: String? = null, + val px_170x170: String? = null, + val px_50x50: String? = null, +) : Serializable { + + fun findMaxSizeUrl(): String? { + if (original != null) { + return original + } + + if (large != null) { + return large + } + + if (medium != null) { + return medium + } + + if (square_medium != null) { + return square_medium + } + + if (px_170x170 != null) { + return px_170x170 + } + + if (px_50x50 != null) { + return px_50x50 + } + + if (px_16x16 != null) { + return px_16x16 + } + + return null + } +} + +data class ErrorResponse( + val error: Error? = null +) : Serializable + +data class WebApiError( + val error: Boolean? = null, + val message: String? = null, +) : Serializable + +data class Error( + val message: String? = null, + val reason: String? = null, + val user_message: String? = null, + val user_message_details: UserMessageDetails? = null +) : Serializable { + + fun displayMessage(): String? { + if (message?.isNotEmpty() == true) { + return message + } + + if (reason?.isNotEmpty() == true) { + return reason + } + + if (user_message?.isNotEmpty() == true) { + return user_message + } + + return null + } +} + +class UserMessageDetails : Serializable + +data class UserResponse( + val profile: Profile? = null, + val profile_publicity: ProfilePublicity? = null, + val user: User? = null, + val workspace: Workspace? = null +) { + + fun isPremium(): Boolean { + return profile?.is_premium == true + } +} + +data class Profile( + val address_id: Int? = null, + val background_image_url: String? = null, + val birth: String? = null, + val birth_day: String? = null, + val birth_year: Int? = null, + val country_code: String? = null, + val gender: String? = null, + val is_premium: Boolean? = null, + val is_using_custom_profile_image: Boolean? = null, + val job: String? = null, + val job_id: Int? = null, + val pawoo_url: Any? = null, + val region: String? = null, + val total_follow_users: Int? = null, + val total_illust_bookmarks_public: Int? = null, + val total_illust_series: Int? = null, + val total_illusts: Int? = null, + val total_manga: Int? = null, + val total_mypixiv_users: Int? = null, + val total_novel_series: Int? = null, + val total_novels: Int? = null, + val twitter_account: String? = null, + val twitter_url: String? = null, + val webpage: Any? = null +) + +data class ProfilePublicity( + val birth_day: String? = null, + val birth_year: String? = null, + val gender: String? = null, + val job: String? = null, + val pawoo: Boolean? = null, + val region: String? = null +) + +data class Workspace( + val chair: String? = null, + val comment: String? = null, + val desk: String? = null, + val desktop: String? = null, + val monitor: String? = null, + val mouse: String? = null, + val music: String? = null, + val pc: String? = null, + val printer: String? = null, + val scanner: String? = null, + val tablet: String? = null, + val tool: String? = null, + val workspace_image_url: Any? = null +) + +interface KListShow { + + val displayList: List + + val nextPageUrl: String? +} + +data class UserPreview( + val illusts: List? = null, + val is_muted: Boolean? = null, + val novels: List? = null, + val user: User? = null +) : Serializable + +data class UserPreviewResponse( + val user_previews: List = listOf(), + val next_url: String? = null +) : Serializable, KListShow { + override val displayList: List get() = user_previews + override val nextPageUrl: String? get() = next_url +} + +data class TrendingTagsResponse( + val trend_tags: List = listOf(), + val next_url: String? = null +) : Serializable, KListShow { + override val displayList: List + get() = trend_tags + override val nextPageUrl: String? + get() = next_url +} + +data class TrendingTag( + val tag: String? = null, + val translated_name: String? = null, + val illust: Illust? = null, +) : Serializable + + +data class ArticlesResponse( + val spotlight_articles: List
= listOf(), + val next_url: String? = null +) : Serializable, KListShow
{ + override val displayList: List
get() = spotlight_articles + override val nextPageUrl: String? get() = next_url +} + +data class Article( + val id: Long, + val title: String? = null, + val pure_title: String? = null, + val thumbnail: String? = null, + val article_url: String? = null, + val publish_date: String? = null, + val category: String? = null, + val subcategory_label: String? = null +) : Serializable, ModelObject { + override val objectUniqueId: Long + get() = id + override val objectType: Int + get() = ObjectSpec.ARTICLE +} + +data class SingleIllustResponse( + val illust: Illust? = null, +) : Serializable + +data class GifInfoResponse( + val illustId: Long, + val ugoira_metadata: UgoiraMetaData? = null, +) : Serializable, ModelObject { + override val objectUniqueId: Long + get() = illustId + override val objectType: Int + get() = ObjectSpec.GIF_INFO +} + +data class UgoiraMetaData( + val zip_urls: ZipUrl? = null, + val frames: List? = null +) : Serializable + +data class ZipUrl( + val medium: String? = null, +) : Serializable + +data class GifFrame( + val file: String? = null, + val delay: Int? = null, +) : Serializable + +data class AddCommentResponse( + val comment: Comment? = null, +) : Serializable + +data class Comment( + val comment: String? = null, + val date: String? = null, + val has_replies: Boolean = false, + val id: Long = 0, + val stamp: Stamp? = null, + val user: User = User() +) : Serializable { + + fun displayCommentDate(): String { + return DateParse.displayCreateDate(date) + } +} + +data class Stamp( + val stamp_id: Long = 0, + val stamp_url: String? = null, +) + +data class CommentResponse( + val comments: List = listOf(), + val next_url: String? = null +): Serializable, KListShow { + override val displayList: List + get() = comments + override val nextPageUrl: String? + get() = next_url +} + +data class WebResponse ( + val error: Boolean? = null, + val message: String? = null, + val body: T? = null, +) : Serializable + +data class RelatedUserBody ( + val thumbnails: List? = null, + val users: List? = null, +) : Serializable + +data class WebRecmdBody ( + val thumbnails: List? = null, + val popularTags: TagsBody? = null, + val recommendTags: TagsBody? = null, + val recommendByTags: TagsBody? = null, +) : Serializable + + +data class TagsBody ( + val illust: List? = null, +) : Serializable + +data class SingleRecommend ( + val tag: String? = null, + val ids: List? = null, +) : Serializable + +data class WebUser( + val userId: Long? = null, + val partial: Long? = null, + val comment: String? = null, + val name: String? = null, + val image: String? = null, + val imageBig: String? = null, + val followedBack: Boolean? = null, + val premium: Boolean? = null, + val isFollowed: Boolean? = null, + val isMypixiv: Boolean? = null, + val isBlocking: Boolean? = null, + val acceptRequest: Boolean? = null +) : Serializable + +data class WebIllust( + val alt: String? = null, + val bookmarkData: Any? = null, + val createDate: String? = null, + val description: String? = null, + val height: Int, + val id: Long = 0L, + val illustType: Int? = null, + val isBookmarkable: Boolean? = null, + val isMasked: Boolean? = null, + val isUnlisted: Boolean? = null, + val pageCount: Int? = null, + val profileImageUrl: String? = null, + val restrict: Int? = null, + val sl: Int? = null, + val tags: List? = null, + val title: String? = null, + val titleCaptionTranslation: TitleCaptionTranslation? = null, + val updateDate: String? = null, + val url: String? = null, + val userId: String? = null, + val userName: String? = null, + val width: Int, + val xRestrict: Int? = null +) : Serializable + + +data class TitleCaptionTranslation( + val workCaption: Any, + val workTitle: Any +) : Serializable + +data class WaitingPage ( + val thumbnails: ThumbnailBody? = null +) : Serializable + + +data class ThumbnailBody ( + val illust: List? = null, +) : Serializable + +data class ListIllustBody ( + val illusts: List? = null, +) : Serializable + +data class Novel( + val caption: String? = null, + val create_date: String? = null, + val id: Long, + val image_urls: ImageUrls? = null, + val is_bookmarked: Boolean? = null, + val is_muted: Boolean? = null, + val is_mypixiv_only: Boolean? = null, + val is_original: Boolean? = null, + val is_x_restricted: Boolean? = null, + val page_count: Int? = null, + val restrict: Int? = null, + val series: Series? = null, + val tags: List? = null, + val text_length: Int? = null, + val title: String? = null, + val total_bookmarks: Int? = null, + val total_comments: Int? = null, + val total_view: Int? = null, + val user: User? = null, + val visible: Boolean? = null, + val x_restrict: Int? = null +) : Serializable, ModelObject { + override val objectUniqueId: Long + get() = id + override val objectType: Int + get() = ObjectSpec.POST +} + +data class Series ( + val id: Long, + val title: String? = null, +) : Serializable + +data class NovelResponse( + val novels: List = listOf(), + val next_url: String? = null +) : Serializable, KListShow { + override val displayList: List get() = novels + override val nextPageUrl: String? get() = next_url +} + +data class NovelText( + val coverUrl: String? = null, + val glossaryItems: List? = null, + val id: String? = null, + val illusts: List? = null, + val images: List? = null, + val marker: Any? = null, + val replaceableItemIds: List? = null, + val seriesId: String? = null, + val seriesNavigation: SeriesNavigation? = null, + val text: String? = null, + val userId: String? = null +) + +data class SeriesNavigation( + val nextNovel: NextNovel? = null, + val prevNovel: Any? = null +) + +data class NextNovel( + val contentOrder: String? = null, + val coverUrl: String? = null, + val id: Int? = null, + val title: String? = null, + val viewable: Boolean? = null, + val viewableMessage: Any? = null +) \ No newline at end of file diff --git a/app/src/main/java/ceui/loxia/ObjectPool.kt b/app/src/main/java/ceui/loxia/ObjectPool.kt index 768357c84..34e947781 100644 --- a/app/src/main/java/ceui/loxia/ObjectPool.kt +++ b/app/src/main/java/ceui/loxia/ObjectPool.kt @@ -1,4 +1,116 @@ package ceui.loxia -class ObjectPool { +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import ceui.lisa.models.IllustsBean +import ceui.lisa.models.ModelObject +import ceui.lisa.models.ObjectSpec +import ceui.lisa.models.UserBean +import java.io.Serializable +import kotlin.reflect.KClass + + +data class ObjectKey( + val id: Long, + val type: Int +) : Serializable + +object ObjectPool { + + val store = mutableMapOf>() + + fun putUserPreview(preview: UserPreview) { + preview.user?.let { user -> + update(user) + } + + preview.illusts?.forEach { illust -> + update(illust) + } + } + + fun updateIllust(illust: IllustsBean) { + update(illust) + illust.user?.let { user -> + update(user) + } + } + + fun followUser(userId: Long) { + val exist = get(userId).value ?: return + exist.isIs_followed = true + update(exist) + } + + fun unFollowUser(userId: Long) { + val exist = get(userId).value ?: return + exist.isIs_followed = false + update(exist) + } + + inline fun get(id: Long): LiveData { + return getFromMap(ObjectT::class, id) + } + + fun getFromMap(objClass: KClass, id: Long): LiveData { + val key = ObjectKey(id, findObjectSpec(objClass)) + val storedLiveData = store[key] + return (if (storedLiveData == null) { + val newly = MutableLiveData() + store[key] = newly + newly + } else { + storedLiveData + }) as LiveData + } + + inline fun update(obj: ObjectT, isFullVersion: Boolean = false) { + return updateObjectPool(obj, isFullVersion) + } + + inline fun updateObjectPool(obj: ObjectT, isFullVersion: Boolean) { + val key = ObjectKey(obj.objectUniqueId, obj.objectType) + val storedObject = store[key] + if (storedObject == null) { + store[key] = MutableLiveData(obj) + } else { + try { + if (isFullVersion) { + storedObject.value = obj + } else { +// val lastValue = storedObject.value +// if (lastValue != null) { +// storedObject.value = merge(ObjectT::class, lastValue as ObjectT, obj) +// } else { + storedObject.value = obj +// } + } + } catch (ex: Exception) { + storedObject.postValue(obj) + } + } + Log.d("updateObjectPool", "对象池大小:${store.size}") + } + + private fun findObjectSpec(objClass: KClass): Int { + val classSimpleName = objClass.simpleName ?: return ObjectSpec.UNKNOWN + return when (classSimpleName) { + "IllustsBean", "Novel" -> { + ObjectSpec.POST + } + "UserBean" -> { + ObjectSpec.USER + } + "Article" -> { + ObjectSpec.ARTICLE + } + "GifInfoResponse" -> { + ObjectSpec.GIF_INFO + } + else -> { + ObjectSpec.UNKNOWN + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/ceui/refactor/KUtils.kt b/app/src/main/java/ceui/refactor/KUtils.kt index 84b19404d..b61ea69ef 100644 --- a/app/src/main/java/ceui/refactor/KUtils.kt +++ b/app/src/main/java/ceui/refactor/KUtils.kt @@ -3,6 +3,9 @@ package ceui.refactor import android.animation.AnimatorInflater import android.content.res.Resources import android.view.View +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce import androidx.fragment.app.Fragment import androidx.fragment.app.findFragment import ceui.lisa.R @@ -64,4 +67,21 @@ internal val screenWidth: Int get() = Resources.getSystem().displayMetrics.widthPixels internal val screenHeight: Int - get() = Resources.getSystem().displayMetrics.heightPixels \ No newline at end of file + get() = Resources.getSystem().displayMetrics.heightPixels + + +fun View.animateFadeIn() { + SpringAnimation(this, DynamicAnimation.ALPHA, 1F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 15F + start() + } +} + +fun View.animateFadeOut() { + SpringAnimation(this, DynamicAnimation.ALPHA, 0F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 15F + start() + } +} \ No newline at end of file diff --git a/app/src/main/java/ceui/refactor/ViewUtils.kt b/app/src/main/java/ceui/refactor/ViewUtils.kt new file mode 100644 index 000000000..4539ddfec --- /dev/null +++ b/app/src/main/java/ceui/refactor/ViewUtils.kt @@ -0,0 +1,880 @@ +package ceui.refactor + +import android.animation.* +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Resources +import android.graphics.* +import android.os.Build +import android.text.StaticLayout +import android.text.TextPaint +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.util.Size +import android.util.TypedValue +import android.view.* +import android.view.animation.* +import android.widget.* +import androidx.annotation.ColorInt +import androidx.annotation.WorkerThread +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.isVisible +import androidx.databinding.BindingAdapter +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import ceui.lisa.R +import com.google.android.material.appbar.AppBarLayout +import java.io.File +import java.io.FileOutputStream +import kotlin.math.min +import kotlin.math.roundToInt + +fun View.setScale(scale: Float) { + scaleX = scale + scaleY = scale +} + +fun View.getLayoutWeight(): Float { + return (layoutParams as? LinearLayout.LayoutParams)?.weight ?: 0.0f +} + +fun disableAll(viewGroup: ViewGroup) { + viewGroup.isEnabled = false + for (index in 0 until viewGroup.childCount) { + val child = viewGroup.getChildAt(index) + if (child is ViewGroup) { + disableAll(child) + } else { + child.isEnabled = false + child.alpha = 0.3F + } + } +} + +fun ViewGroup.getNotGoneChildren(): List { + val res = mutableListOf() + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child.visibility != View.GONE) { + res.add(child) + } + } + + return res +} + +fun View.getAncestorsDesc(): String { + + val chain = mutableListOf() + chain.add(this.toString()) + var itr = this.parent + while (itr != null) { + + chain.add(itr.toString()) + itr = itr.parent + } + + return chain.mapIndexed { index, string -> "${index} ${string}" }.joinToString("\n", prefix = "\n") +} + +inline fun View.findAncestorOrSelfOrNull(predicate: (View) -> Boolean): View? { + var itr: View? = this + while (itr != null) { + if (predicate(itr)) { + return itr + } + itr = (itr.parent as? View) + } + return null +} + +inline fun View.findAncestorOrNull(): T? { + var itr = this.parent + while (itr != null) { + if (itr is T) { + return itr + } + itr = itr.parent + } + return null +} + +inline fun View.findAncestorOrSelfOrNull(): T? { + var itr = this as? ViewParent + while (itr != null) { + if (itr is T) { + return itr + } + itr = itr.parent + } + return null +} + +inline fun ViewGroup.findChildOrNull(): T? { + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child is T) { + return child + } + } + + return null +} + +inline fun View.findChildOrSelfOrNull(): T? { + if (this is ViewGroup) { + return this.findChildOrNull() + } else { + return this as? T + } +} + + +fun ViewGroup.forEachDescendants(visitor: (view: View) -> Boolean) { + forEachDescendantsImpl(visitor) +} +// true to stop +fun ViewGroup.forEachDescendantsImpl(visitor: (view: View) -> Boolean): Boolean { + for (i in 0 until childCount) { + val child = getChildAt(i) + if (visitor(child)) { + return true + } + + val vg = child as? ViewGroup + if (vg != null) { + if (vg.forEachDescendantsImpl(visitor)) { + return true + } + } + } + return false +} + +inline fun View.findSibling(): T? { + return (parent as? ViewGroup)?.findChildOrNull() +} + + +val View.size: Size + get() = Size(width, height) + +@set:BindingAdapter("isSelected") +var View.isSelected + get() = isSelected + set(value) { + isSelected = value + } + +@set:BindingAdapter("visibleOrGone") +var View.visibleOrGone + get() = visibility == View.VISIBLE + set(value) { + visibility = if (value) View.VISIBLE else View.GONE + } + +@set:BindingAdapter("invisibleOrGone") +var View.invisibleOrGone + get() = visibility == View.INVISIBLE + set(value) { + visibility = if (value) View.INVISIBLE else View.GONE + } + +@set:BindingAdapter("visibleOrInvisible") +var View.visibleOrInvisible + get() = visibility == View.VISIBLE + set(value) { + visibility = if (value) View.VISIBLE else View.INVISIBLE + } + + +@set:BindingAdapter("isActivated") +var View.isActivated + get() = isActivated + set(value) { + isActivated = value + } + +@set:BindingAdapter("isEnabled") +var View.isEnabled + get() = isEnabled + set(value) { + isEnabled = value + } + +@set:BindingAdapter("isEnabledAndDark") +var View.isEnabledAndDark + get() = isEnabled + set(value) { + isEnabled = value + alpha = if (value) { + 1F + } else { + 0.5F + } + } + +@set:BindingAdapter("isClickableAndDark") +var View.isClickableAndDark + get() = isClickable + set(value) { + isClickable = value + alpha = if (value) { + 1F + } else { + 0.5F + } + } + +@set:BindingAdapter("updateText") +var TextView.updateText: CharSequence? + get() = text + set(value) { + text = value + requestLayout() + } +@set:BindingAdapter("updateTextOneLine") +var TextView.updateTextOneLine: CharSequence? + get() = text + set(value) { + text = value.toString().replace("\n"," ").trim() + requestLayout() + } + +fun Context.screenDisplay(): DisplayMetrics { + val displayMetrics = DisplayMetrics() + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + windowManager.defaultDisplay.getRealMetrics(displayMetrics) + return displayMetrics +} + +fun View.screenDisplay(): DisplayMetrics { + return context.screenDisplay() +} + +fun View.divideCount(unit: Int, offset: Int): Int { + val metrics = screenDisplay() + val screenWidth = (metrics.widthPixels.toFloat() / metrics.density).toInt() + return (screenWidth - offset) / unit +} + +fun View.proportionHeight(height: Int): Float { + val metrics = screenDisplay() + val screenHeight = metrics.heightPixels.toFloat() / metrics.density + return min(1.0f, height.toFloat() / screenHeight) +} + + +fun View.convertRectToWindowSpace(rect: Rect): Rect { + val locations = intArrayOf(0, 0) + getLocationInWindow(locations) + return Rect( + rect.left + locations[0], + rect.top + locations[1], + rect.right + locations[0], + rect.bottom + locations[1] + ) +} + +fun View.convertYToWindowSpace(y: Int): Int { + val locations = intArrayOf(0, 0) + getLocationInWindow(locations) + return y + locations[1] +} + +val View.frame get() = Rect(left, top, right, bottom) + + + +fun TextView.makeUnderline() { + paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG +} + +fun View.makeGoneIf(f: Boolean) { + visibility = if (f) View.GONE else View.VISIBLE +} + +fun List.makeOneVisible(visibleView: View?) { + forEach { + it.makeGoneIf(it != visibleView) + } +} +fun List.makeOneSelected(selected: View?) { + forEach { + it.isSelected = (it == selected) + } +} + +class InterceptTouchFrameLayout(context: Context, attrs: AttributeSet?, defStyle: Int, defStyleRes: Int) + : FrameLayout(context, attrs, defStyle, defStyleRes) { + + constructor(context: Context) : this(context, null, 0, 0) + + constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyle: Int): this(context, attrs, defStyle, 0) + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } +} + +object Justify { + + data class LayoutInfo(val maxChildCount: Int, val actualSpacing: Float) + + fun getColumnLayoutInfo(availableWidth: Float, childWidth: Float, minSpacing: Float): LayoutInfo { + require(childWidth > 10) + + var consumedWidth = 0F + var triedChildCount = 0 + while (consumedWidth < availableWidth) { + consumedWidth += childWidth + if (triedChildCount != 0) { + consumedWidth += minSpacing + } + + ++triedChildCount + } + + val maxChildCount = if (triedChildCount == 0) 0 else (triedChildCount - 1) + if (maxChildCount <= 1) { + return LayoutInfo(maxChildCount, 0F) + } + + val actualSpacing = (availableWidth - maxChildCount * childWidth).toFloat() / (maxChildCount - 1) + + return LayoutInfo(maxChildCount, actualSpacing) + } +} + +fun measureChildNoPadding( + child: View, + parentWidthMeasureSpec: Int, + parentHeightMeasureSpec: Int +) { + val lp = child.layoutParams + val childWidthMeasureSpec = ViewGroup.getChildMeasureSpec( + parentWidthMeasureSpec, + 0, lp.width + ) + val childHeightMeasureSpec = ViewGroup.getChildMeasureSpec( + parentHeightMeasureSpec, + 0, lp.height + ) + child.measure(childWidthMeasureSpec, childHeightMeasureSpec) +} + + +class TypingTextViewModel: ViewModel() { + val text = MutableLiveData("") +} + +fun ViewPager2.addOnPageSelectedListener(listener: (position: Int) -> Unit) { + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + listener(position) + } + }) +} + + +fun View.toBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap? { + return Bitmap.createBitmap(width, height, config).also { bitmap -> + val radius = screenDisplay().density * 16f + val path = Path().also { + it.addRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), radius, radius, Path.Direction.CW) + } + Canvas(bitmap).also { canvas -> + canvas.clipPath(path) + draw(canvas) + } + } +} + +// returns file path +@WorkerThread +fun Bitmap.saveAsJpgToTempFile(prefix: String, quality: Int, subdir: File?): File { + val tempFile = File.createTempFile(prefix, ".jpg", subdir) + return saveAsJpgToFile(tempFile, quality) +} + +@WorkerThread +fun Bitmap.saveAsJpgToFile(file: File, quality: Int): File { + val outputStream = FileOutputStream(file) + compress(Bitmap.CompressFormat.JPEG, quality, outputStream) + outputStream.flush() + outputStream.close() + + return file +} + +@WorkerThread +fun Bitmap.saveAsPngToTempFile(prefix: String, quality: Int, subdir: File?): File { + val tempFile = File.createTempFile(prefix, ".png", subdir) + return saveAsPngToFile(tempFile, quality) +} + +@WorkerThread +fun Bitmap.saveAsPngToFile(file: File, quality: Int): File { + val outputStream = FileOutputStream(file) + compress(Bitmap.CompressFormat.PNG, quality, outputStream) + outputStream.flush() + outputStream.close() + + return file +} + +@WorkerThread +fun Bitmap.saveAsWebpToFile(file: File, quality: Int): File { + val outputStream = FileOutputStream(file) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + compress(Bitmap.CompressFormat.WEBP_LOSSLESS, quality, outputStream) + } else { + compress(Bitmap.CompressFormat.WEBP, quality, outputStream) + } + outputStream.flush() + outputStream.close() + + return file +} + +fun measureTextWidth(content: String, textSize: Float, resources: Resources): Int { + val paint = TextPaint() + paint.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, resources.displayMetrics) + return StaticLayout.getDesiredWidth(content, paint).toInt() +} + + + +@BindingAdapter("shadowRadius", "shadowColor") +fun TextView.binding_setShadow(radius: Float, @ColorInt color: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setShadowLayer(radius, 0F, 0F, color) + } +} + + +fun AppBarLayout.getAppBarBehavior(): AppBarLayout.Behavior? { + return ((layoutParams as CoordinatorLayout.LayoutParams).behavior as? AppBarLayout.Behavior) +} + + +fun ViewPager2.getRecyclerView(): RecyclerView? { + return findChildOrNull() +} + +//fun View.findViewByIdOrNull(@IdRes id: Int): T? { +// return try { +// findViewById(id) +// } catch (e: java.lang.Exception) { +// null +// } +//} + +fun View.findDescendantOrSelfMatches(predicate: (View) -> Boolean): View? { + if (predicate(this)) + return this + + if (this is ViewGroup) { + for (i in 0 until childCount) { + val child = getChildAt(i) + val target = child.findDescendantOrSelfMatches(predicate) + if (target != null) + return target + } + return null + } else { + return null + } +} + + +fun View.findDescendantMatches(predicate: (View) -> Boolean): View? { + if (this is ViewGroup) { + for (i in 0 until childCount) { + val child = getChildAt(i) + val target = child.findDescendantOrSelfMatches(predicate) + if (target != null) + return target + } + } + + return null +} + +fun View.findAllDescendantsMatches(predicate: (View) -> Boolean): List { + val result = mutableListOf() + if (predicate(this)) + result.add(this) + if (this is ViewGroup) { + for (i in 0 until childCount) { + val child = getChildAt(i) + val target = child.findAllDescendantsMatches(predicate) + target.forEach { result.add(it) } + } + } + + return result +} + +inline fun View.findDescendant(): T? { + return findDescendantMatches { + it is T + } as? T +} + +inline fun View.findDescendantOrSelf(): T? { + return findDescendantOrSelfMatches { + it is T + } as? T +} + +fun View.isDescendantOf(ancestor: View): Boolean { + var itr: View? = this + while (itr != null) { + if (itr == ancestor) + return true + + itr = itr.parent as? View + } + + return false +} + +fun View.largeEnough(limit: Double = 1.8) : Boolean { + val boundingWidth = this.screenDisplay().widthPixels * 0.88 + val boundingHeight = this.screenDisplay().heightPixels * 0.8 + return boundingHeight / boundingWidth > limit +} + +fun View.dipToPx(dp: Float) : Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics).roundToInt() +} + +fun View.dipToPxF(dp: Float) : Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) +} + +fun View.spToPx(sp: Float) : Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, resources.displayMetrics).roundToInt() +} + +fun View.spToPxF(sp: Float) : Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, resources.displayMetrics) +} + +fun View.animateWiggle() { + val wiggle: Animation = AnimationUtils.loadAnimation(context, R.anim.wiggle) + wiggle.interpolator = CycleInterpolator(3F) + clearAnimation() + startAnimation(wiggle) +} + +fun View.animateWiggleForPasswordCheck() { + val wiggle: Animation = AnimationUtils.loadAnimation(context, R.anim.wiggle_for_password_check) + wiggle.interpolator = CycleInterpolator(1F) + clearAnimation() + startAnimation(wiggle) +} + +fun View.animateInvisible() { + SpringAnimation(this, DynamicAnimation.ALPHA, 0F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = SpringForce.STIFFNESS_VERY_LOW + start() + } +} + +fun View.animateVisible() { + SpringAnimation(this, DynamicAnimation.ALPHA, 1F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = SpringForce.STIFFNESS_VERY_LOW + start() + } +} + + +fun View.adjustShowKeyboard(height: Float = -dipToPx(150F).toFloat()) { + translationY = 0F + SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, height).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = SpringForce.STIFFNESS_MEDIUM + start() + } +} + +fun View.adjustHideKeyboard() { + SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, 0F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = SpringForce.STIFFNESS_MEDIUM + start() + } +} + +fun View.slideRightToLeft(startOffset: Float = dipToPx(170F).toFloat()) { + translationX = startOffset + SpringAnimation(this, DynamicAnimation.TRANSLATION_X, 0F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 500F + start() + } +} + +fun View.slideLeftToRight(startOffset: Float = dipToPx(170F).toFloat()) { + translationX = 0F + SpringAnimation(this, DynamicAnimation.TRANSLATION_X, startOffset).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 800F + start() + } +} + + +fun View.animateFadeInQuickly(): SpringAnimation { + alpha = 0F + isVisible = true + val anim = SpringAnimation(this, DynamicAnimation.ALPHA, 1F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 300F + } + anim.addEndListener { animation, canceled, value, velocity -> + alpha = 1F + }.start() + return anim +} + +fun View.animateFadeOutQuickly(): SpringAnimation { + alpha = 1F + val anim = SpringAnimation(this, DynamicAnimation.ALPHA, 0F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 300F + } + anim.addUpdateListener { _, value, _ -> + if (value < 0.2F) { + visibleOrInvisible = false + } + }.addEndListener { animation, canceled, value, velocity -> + alpha = 0F + }.start() + return anim +} + + +fun View.animateMeteor() { + val x = measuredWidth.toFloat() + val screenWidth = screenDisplay().widthPixels.toFloat() + SpringAnimation(this, DynamicAnimation.TRANSLATION_X, -x).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 50F + start() + } + + val finalTranslationY = translationY + screenWidth + x + + SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, finalTranslationY).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 50F + start() + } +} + +fun View.animateSlideX() { + SpringAnimation(this, DynamicAnimation.TRANSLATION_X, 0F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 100F + start() + } +} + +fun View.animateSlideY() { + SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, 0F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 100F + start() + } +} + +fun View.animateSlideXY() { + /** + * spring.dampingRatio = 0.75F + * spring.stiffness = 100F + * delay(550L) + */ + + SpringAnimation(this, DynamicAnimation.TRANSLATION_X, 0F).apply { + spring.dampingRatio = 0.75F + spring.stiffness = 180F + start() + } + + SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, 0F).apply { + spring.dampingRatio = 0.75F + spring.stiffness = 180F + start() + } +} + +fun View.animateScale() { + SpringAnimation(this, DynamicAnimation.SCALE_X, 1.1F).apply { + spring.dampingRatio = 0.4F + spring.stiffness = 120F + start() + } + + SpringAnimation(this, DynamicAnimation.SCALE_Y, 1.1F).apply { + spring.dampingRatio = 0.4F + spring.stiffness = 120F + start() + } +} + +fun View.animateZoomOut() { + val finalPosition = -dipToPx(18F).toFloat() + SpringAnimation(this, DynamicAnimation.TRANSLATION_Y, finalPosition).apply { + spring.dampingRatio = 0.6F + spring.stiffness = 80F + start() + } + SpringAnimation(this, DynamicAnimation.SCALE_X, 0.84F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 30F + start() + } + SpringAnimation(this, DynamicAnimation.SCALE_Y, 0.84F).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 30F + start() + } +} + +fun View.animateScaleXY(position: Float) { + SpringAnimation(this, DynamicAnimation.SCALE_X, position).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY + spring.stiffness = SpringForce.STIFFNESS_LOW + start() + } + SpringAnimation(this, DynamicAnimation.SCALE_Y, position).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY + spring.stiffness = SpringForce.STIFFNESS_LOW + start() + } +} + +fun View.animateScaleXYNoBouncy(position: Float) { + SpringAnimation(this, DynamicAnimation.SCALE_X, position).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 100F + start() + } + SpringAnimation(this, DynamicAnimation.SCALE_Y, position).apply { + spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + spring.stiffness = 100F + start() + } +} + +fun View.animateScaleXYAndAlpha(duration: Long) { + val animScaleX = ObjectAnimator.ofFloat(this, "scaleX",1F, 2F).apply { + repeatMode = ValueAnimator.RESTART + } + val animScaleY = ObjectAnimator.ofFloat(this, "scaleY",1F, 2F).apply { + repeatMode = ValueAnimator.RESTART + } + val animAlpha = ObjectAnimator.ofFloat(this, "alpha", 1F, 0F).apply { + repeatMode = ValueAnimator.RESTART + } + AnimatorSet().apply { + playTogether(animScaleX, animScaleY, animAlpha) + setDuration(duration) + start() + + } +} + +fun View.animateAlpha(duration: Long) { + val animAlpha = ObjectAnimator.ofFloat(this, "alpha", 0F, 1F, 0F).apply { + repeatMode = ValueAnimator.RESTART + } + AnimatorSet().apply { + play(animAlpha) + setDuration(duration) + start() + } +} + +fun View.animateSpin() { + val animation = AnimationUtils.loadAnimation(context, R.anim.rotate_cycle) + animation.interpolator = LinearInterpolator() + animation.repeatCount = Animation.INFINITE + startAnimation(animation) +} + +fun View.animateAlpha() { + val animation = AnimationUtils.loadAnimation(context, R.anim.alpha_cycle) + animation.interpolator = LinearInterpolator() + animation.repeatCount = Animation.INFINITE + startAnimation(animation) +} + +/** @see [Lookup resource name](https://stackoverflow.com/questions/10137692/how-to-get-resource-name-from-resource-id) + */ +fun View.getResIdName(resources: Resources?): String? { + if (resources == null) return null + return try { + resources.getResourceEntryName(this.id) + } catch (ignored: Throwable) { + null + } +} + +fun View.getResIdName(): String? { + return getResIdName(context.resources) +} + + + +fun EditText.getFocusedLiveData(): LiveData { + val ret = MutableLiveData(isFocused) + setOnFocusChangeListener { v, hasFocus -> + ret.value = hasFocus + } + + return ret +} + +fun EditText.moveCursorToEnd() { + setSelection(text?.length ?: 0) +} + +fun TextView.getTouchOffset(event: MotionEvent): Int { + val widget = this + var x = event.x.toInt() + var y = event.y.toInt() + x -= widget.totalPaddingLeft + y -= widget.totalPaddingTop + x += widget.scrollX + y += widget.scrollY + val layout = widget.layout + val line = layout.getLineForVertical(y) + return layout.getOffsetForHorizontal(line, x.toFloat()) +} + + +fun View.findActivity(): Activity? { + var context = this.context + while (context is ContextWrapper) { + if (context is Activity) { + return context + } + context = context.baseContext + } + return null +} diff --git a/app/src/main/res/anim/alpha_cycle.xml b/app/src/main/res/anim/alpha_cycle.xml new file mode 100644 index 000000000..589f89add --- /dev/null +++ b/app/src/main/res/anim/alpha_cycle.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/anim/rotate_cycle.xml b/app/src/main/res/anim/rotate_cycle.xml new file mode 100644 index 000000000..d67e36809 --- /dev/null +++ b/app/src/main/res/anim/rotate_cycle.xml @@ -0,0 +1,26 @@ + + + diff --git a/app/src/main/res/anim/wiggle.xml b/app/src/main/res/anim/wiggle.xml new file mode 100644 index 000000000..5f2a80648 --- /dev/null +++ b/app/src/main/res/anim/wiggle.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/wiggle_for_password_check.xml b/app/src/main/res/anim/wiggle_for_password_check.xml new file mode 100644 index 000000000..7ca8cd597 --- /dev/null +++ b/app/src/main/res/anim/wiggle_for_password_check.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_new_user.xml b/app/src/main/res/layout/activity_new_user.xml index 995bf1c14..8ffd208c2 100644 --- a/app/src/main/res/layout/activity_new_user.xml +++ b/app/src/main/res/layout/activity_new_user.xml @@ -105,7 +105,7 @@ - - + + + + - + - + android:layout_height="match_parent"> - + android:background="@color/white"> + - + android:layout_above="@+id/bottom_linear"> - + android:layout_height="match_parent" + android:background="@color/black"> + android:layout_height="match_parent"> - + android:layout_height="match_parent" + android:layout_above="@+id/helper_view" + android:clipChildren="false"> - + + - - + - + - + - + + - - - - - - - - - - + app:behavior_peekHeight="180dp" + app:layout_behavior="@string/bottom_sheet_behavior" + app:qmui_backgroundColor="@color/fragment_center" + app:qmui_borderWidth="0dp" + app:qmui_radiusTopLeft="20dp" + app:qmui_radiusTopRight="20dp" + android:clickable="true" + android:focusable="true"> - - - - - + android:tag="12345888"> - + - + + - - - + + android:textSize="20sp" + app:autoSizeMaxTextSize="20sp" + app:autoSizeMinTextSize="6sp" + app:autoSizeTextType="uniform" + android:textStyle="bold"> - + app:qmui_radius="16dp"> - + - + - - + - + - + - + + + + + + + + + + + + + + + android:layout_height="0.5dp" + android:layout_marginStart="@dimen/sixteen_dp" + android:layout_marginEnd="@dimen/sixteen_dp" + android:background="@color/settings_divider"> + + + + + android:orientation="vertical"> - + android:baselineAligned="false" + android:paddingTop="@dimen/six_dp" + android:paddingBottom="@dimen/six_dp"> - + android:layout_weight="1"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + + - - - - - - - - - - - - + android:layout_marginStart="@dimen/sixteen_dp" + android:layout_marginTop="@dimen/four_dp" + android:layout_marginEnd="@dimen/sixteen_dp" + android:layout_marginBottom="@dimen/eight_dp" + android:orientation="horizontal"> - - - - - + - - + - - - + - - - + - - - + - + - - + - + - + + + + + + + + - + - + - - + android:background="@android:color/transparent" + android:elevation="@dimen/six_dp" + android:theme="@style/AppTheme.AppBarOverlay" + app:navigationIcon="@drawable/ic_arrow_back_white_shadow" + app:popupTheme="@style/AppTheme.PopupOverlay" + app:title=" " + app:titleTextColor="@android:color/white"> - + - + - - - - - + android:layout_height="match_parent"> - - - - - - - + android:orientation="vertical" + android:layout_gravity="center" + android:gravity="center_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + + + + + - - - - - - + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index eda650542..fb029411b 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -33,4 +33,12 @@ android:name="flag_object_type" app:argType="integer" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39e61676f..d739bc06f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,8 +19,8 @@ 居然啥也没有 居然啥也没有 居然啥也没有 - + 关注 - 取消关注 + 关注 + 取关 再按一次退出~ 删除全部 筛选 @@ -667,4 +667,6 @@ 屏蔽此用户的作品 取消屏蔽此用户的作品 将此用户加入黑名单 + 关注 + 取关 diff --git a/models/src/main/java/ceui/lisa/models/IllustsBean.java b/models/src/main/java/ceui/lisa/models/IllustsBean.java index f34bca4a4..002e5cf56 100644 --- a/models/src/main/java/ceui/lisa/models/IllustsBean.java +++ b/models/src/main/java/ceui/lisa/models/IllustsBean.java @@ -2,9 +2,8 @@ import java.io.Serializable; import java.util.List; -import java.util.stream.Collectors; -public class IllustsBean implements Serializable, Starable, Deduplicatable { +public class IllustsBean implements Serializable, Starable, Deduplicatable, ModelObject { /** * id : 73949833 * title : 命に繋がる魂の絆 @@ -348,4 +347,14 @@ public String[] getTagNames(){ public Object getDuplicateKey() { return id; } + + @Override + public long getObjectUniqueId() { + return id; + } + + @Override + public int getObjectType() { + return ObjectSpec.POST; + } } diff --git a/app/src/main/java/ceui/loxia/ObjectSpec.kt b/models/src/main/java/ceui/lisa/models/ModelObject.kt similarity index 58% rename from app/src/main/java/ceui/loxia/ObjectSpec.kt rename to models/src/main/java/ceui/lisa/models/ModelObject.kt index e3056f8f7..57356e375 100644 --- a/app/src/main/java/ceui/loxia/ObjectSpec.kt +++ b/models/src/main/java/ceui/lisa/models/ModelObject.kt @@ -1,4 +1,9 @@ -package ceui.loxia +package ceui.lisa.models + +interface ModelObject { + val objectUniqueId: Long + val objectType: Int +} object ObjectSpec { const val UNKNOWN = 0 diff --git a/models/src/main/java/ceui/lisa/models/UserBean.java b/models/src/main/java/ceui/lisa/models/UserBean.java index c0a3d7353..33f35b954 100644 --- a/models/src/main/java/ceui/lisa/models/UserBean.java +++ b/models/src/main/java/ceui/lisa/models/UserBean.java @@ -2,7 +2,7 @@ import java.io.Serializable; -public class UserBean implements Serializable, UserContainer, Starable { +public class UserBean implements Serializable, UserContainer, Starable, ModelObject { /** * profile_image_urls : {"px_16x16":"https://i.pximg.net/user-profile/img/2018/06/20/23/27/47/14384932_69771f95cafdac1a1d3da88fcfe4ecab_16.jpg","px_50x50":"https://i.pximg.net/user-profile/img/2018/06/20/23/27/47/14384932_69771f95cafdac1a1d3da88fcfe4ecab_50.jpg","px_170x170":"https://i.pximg.net/user-profile/img/2018/06/20/23/27/47/14384932_69771f95cafdac1a1d3da88fcfe4ecab_170.jpg"} * id : 31655571 @@ -194,4 +194,14 @@ public String toString() { ", require_policy_agreement=" + require_policy_agreement + '}'; } + + @Override + public long getObjectUniqueId() { + return id; + } + + @Override + public int getObjectType() { + return ObjectSpec.USER; + } }