diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 7284d5c37ab..2ca7eed646d 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -28,7 +28,7 @@ on: sync_cnb: description: "Mirror APKs to CNB Release and sync CNB manifests" required: false - default: true + default: false type: boolean permissions: @@ -143,19 +143,30 @@ jobs: STORE_PASSWORD="${RELEASE_STORE_PASSWORD:-$KEY_PASSWORD}" ALIAS="${RELEASE_KEY_ALIAS:-$KEY_ALIAS}" KEY_PASS="${RELEASE_KEY_PASSWORD:-$STORE_PASSWORD}" + mkdir -p app/signing if [ -z "$KS_BASE64" ] || [ -z "$STORE_PASSWORD" ] || [ -z "$ALIAS" ]; then - echo "::error title=Release signing not configured::Configure RELEASE_KEYSTORE_BASE64, RELEASE_KEY_ALIAS and RELEASE_STORE_PASSWORD in repository secrets." - exit 1 + echo "No signing secrets found, generating debug keystore..." + STORE_PASSWORD="android" + KEY_PASS="android" + ALIAS="androiddebugkey" + keytool -genkeypair -v \ + -keystore app/signing/release.jks \ + -alias "$ALIAS" \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -storepass "$STORE_PASSWORD" \ + -keypass "$KEY_PASS" \ + -dname "CN=Android Debug,O=Android,C=US" + else + echo "Using provided release signing key" + printf '%s' "$KS_BASE64" | base64 --decode > app/signing/release.jks fi - mkdir -p app/signing - printf '%s' "$KS_BASE64" | base64 --decode > app/signing/release.jks { echo "storeFile=$GITHUB_WORKSPACE/app/signing/release.jks" echo "keyAlias=$ALIAS" echo "storePassword=$STORE_PASSWORD" echo "keyPassword=$KEY_PASS" } > local.properties - echo "Release signing configured" + echo "Signing configured (alias=$ALIAS)" - name: Build four release APKs env: @@ -166,10 +177,9 @@ jobs: rm -rf Release/apk ./gradlew \ :app:assembleMobileArm64_v8aRelease \ - :app:assembleMobileArmeabi_v7aRelease \ :app:assembleLeanbackArm64_v8aRelease \ - :app:assembleLeanbackArmeabi_v7aRelease \ - --no-daemon + --no-daemon\ + --no-build-cache - name: Collect artifacts run: | diff --git a/app/build.gradle b/app/build.gradle index 2d1cffe8a17..94cd0362e51 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,8 +26,8 @@ android { applicationId "com.fongmi.android.tv" minSdk libs.versions.minSdk.get().toInteger() targetSdk libs.versions.targetSdk.get().toInteger() - versionCode 556 - versionName "5.5.6" + versionCode 558 + versionName "5.5.8" buildConfigField "String", "BUILD_TIME", "\"${buildTime}\"" buildConfigField "String", "BUILD_TAG", "\"${releaseTag}\"" buildConfigField "String", "MEDIA3_VERSION", "\"${libs.versions.media3.get()}\"" diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/CollectActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/CollectActivity.java index a234a1e5101..e7673e23371 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/CollectActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/CollectActivity.java @@ -15,6 +15,7 @@ import com.bumptech.glide.Glide; import com.fongmi.android.tv.App; +import com.fongmi.android.tv.Product; import com.fongmi.android.tv.R; import com.fongmi.android.tv.api.config.VodConfig; import com.fongmi.android.tv.bean.Collect; @@ -30,6 +31,7 @@ import com.fongmi.android.tv.ui.base.BaseActivity; import com.fongmi.android.tv.ui.custom.CustomScroller; import com.fongmi.android.tv.utils.ResUtil; +import com.fongmi.android.tv.utils.SearchModeStore; import com.github.catvod.crawler.SpiderDebug; import com.google.gson.reflect.TypeToken; @@ -173,13 +175,7 @@ private void saveKeyword() { } private void setSites() { - String siteKey = getSiteKey(); - mSites = new ArrayList<>(); - for (Site site : VodConfig.get().getSites()) { - if (!site.isSearchable()) continue; - if (!siteKey.isEmpty() && !site.getKey().equals(siteKey)) continue; - mSites.add(site); - } + mSites = new ArrayList<>(SearchModeStore.filterSites(VodConfig.get().getSites(), getSiteKey())); SiteHealthStore.sortSites(mSites); } @@ -196,7 +192,7 @@ private void search() { } private int getCount() { - return 5; + return Product.getColumn(); } private int getItemWidth(int count) { diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/HomeActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/HomeActivity.java index 3780ec7a20e..5300ffbbe00 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/HomeActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/HomeActivity.java @@ -32,7 +32,6 @@ import com.fongmi.android.tv.bean.Cache; import com.fongmi.android.tv.bean.Class; import com.fongmi.android.tv.bean.Config; -import com.fongmi.android.tv.bean.Func; import com.fongmi.android.tv.bean.History; import com.fongmi.android.tv.bean.Result; import com.fongmi.android.tv.bean.Site; @@ -59,7 +58,6 @@ import com.fongmi.android.tv.ui.custom.CustomTitleView; import com.fongmi.android.tv.ui.dialog.ExitConfirmDialog; import com.fongmi.android.tv.ui.dialog.SiteDialog; -import com.fongmi.android.tv.ui.presenter.FuncPresenter; import com.fongmi.android.tv.ui.presenter.HeaderPresenter; import com.fongmi.android.tv.ui.presenter.HistoryPresenter; import com.fongmi.android.tv.ui.presenter.ProgressPresenter; @@ -89,7 +87,7 @@ import java.util.Locale; import java.util.Optional; -public class HomeActivity extends BaseActivity implements CustomTitleView.Listener, VodPresenter.OnClickListener, FuncPresenter.OnClickListener, HistoryPresenter.OnClickListener, TypeAdapter.OnClickListener, HomeWebController.Listener { +public class HomeActivity extends BaseActivity implements CustomTitleView.Listener, VodPresenter.OnClickListener, HistoryPresenter.OnClickListener, TypeAdapter.OnClickListener, HomeWebController.Listener { private static final String TV_NORMAL = "tv-normal"; private static final String TV_TOOLBAR_HIDDEN = "tv-toolbar-hidden"; @@ -98,7 +96,6 @@ public class HomeActivity extends BaseActivity implements CustomTitleView.Listen private ActivityHomeBinding mBinding; private ArrayObjectAdapter mHistoryAdapter; - private ArrayObjectAdapter mFuncAdapter; private ArrayObjectAdapter mAdapter; private HistoryPresenter mPresenter; private SiteViewModel mViewModel; @@ -155,7 +152,10 @@ protected void initView(Bundle savedInstanceState) { private void initAfterFirstFrame() { SpiderDebug.log("startup", "home first frame cost=%sms", System.currentTimeMillis() - App.time()); App.post(this::initConfig, 80); - App.post(() -> PermissionUtil.requestFile(this, allGranted -> PermissionUtil.requestNotify(this)), 1800); + App.post(() -> PermissionUtil.requestFile(this, allGranted -> { + PermissionUtil.requestNotify(this); + if (allGranted) initConfig(); + }), 1800); App.post(() -> DLNARendererService.start(this), 2500); } @@ -174,6 +174,7 @@ public boolean onPreDraw() { @Override protected void initEvent() { mBinding.title.setListener(this); + initFuncBar(); mBinding.toolbar.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { syncNativeContentInset(); syncWebOverlayLayout(); @@ -258,7 +259,6 @@ private void setRecyclerView() { selector.addPresenter(String.class, new ProgressPresenter()); selector.addPresenter(Vod.class, new VodPresenter(this, Style.list())); selector.addPresenter(ListRow.class, new CustomRowPresenter(16), VodPresenter.class); - selector.addPresenter(ListRow.class, new CustomRowPresenter(16), FuncPresenter.class); selector.addPresenter(ListRow.class, new CustomRowPresenter(16, FocusHighlight.ZOOM_FACTOR_SMALL, HorizontalGridView.FOCUS_SCROLL_ALIGNED), HistoryPresenter.class); mBinding.recycler.setAdapter(new ItemBridgeAdapter(mAdapter = new ArrayObjectAdapter(selector))); mBinding.recycler.setVerticalSpacing(ResUtil.dp2px(16)); @@ -308,7 +308,6 @@ private boolean isHomeCategoryResult(Result result) { private void setAdapter() { mHistoryAdapter = new ArrayObjectAdapter(mPresenter = new HistoryPresenter(this)); - mAdapter.add(new ListRow(mFuncAdapter = new ArrayObjectAdapter(new FuncPresenter(this)))); mAdapter.add(R.string.home_history); mAdapter.add(R.string.home_recommend); } @@ -460,13 +459,8 @@ private void addGrid(List items, Style style) { } private void setFunc() { - List items = new ArrayList<>(); - if (LiveConfig.hasUrl()) items.add(Func.create(R.string.home_live)); - items.add(Func.create(R.string.home_search)); - items.add(Func.create(R.string.home_keep)); - items.add(Func.create(R.string.home_push)); - items.add(Func.create(R.string.home_setting)); - mFuncAdapter.setItems(items, new BaseDiffCallback()); + mBinding.funcLive.setVisibility(LiveConfig.hasLoadedLives() ? View.VISIBLE : View.GONE); + initFuncBar(); } private void getHistory() { @@ -583,20 +577,32 @@ public void error(String msg) { }; } - @Override - public void onItemClick(Func item) { - if (item.getResId() == R.string.home_live) LiveActivity.start(this); - else if (item.getResId() == R.string.home_keep) KeepActivity.start(this); - else if (item.getResId() == R.string.home_push) PushActivity.start(this); - else if (item.getResId() == R.string.home_search) SearchActivity.start(this); - else if (item.getResId() == R.string.home_setting) SettingActivity.start(this); - } + private void initFuncBar() { + mBinding.funcLive.setOnClickListener(v -> LiveActivity.start(this)); + mBinding.funcSearch.setOnClickListener(v -> SearchActivity.start(this)); + mBinding.funcSearch.setOnLongClickListener(v -> { + SearchActivity.start(this, "", getHome().getKey()); + return true; + }); + mBinding.funcKeep.setOnClickListener(v -> KeepActivity.start(this)); + mBinding.funcPush.setOnClickListener(v -> PushActivity.start(this)); + mBinding.funcSetting.setOnClickListener(v -> SettingActivity.start(this)); - @Override - public boolean onLongClick(Func item) { - if (item.getResId() != R.string.home_search) return false; - SearchActivity.start(this, "", getHome().getKey()); - return true; + View[] buttons = {mBinding.funcLive, mBinding.funcSearch, mBinding.funcKeep, mBinding.funcPush, mBinding.funcSetting}; + List visibleButtons = new ArrayList<>(); + for (View b : buttons) if (b.getVisibility() == View.VISIBLE) visibleButtons.add(b); + + if (visibleButtons.isEmpty()) { + mBinding.title.setNextFocusRightId(View.NO_ID); + } else { + for (int i = 0; i < visibleButtons.size(); i++) { + View cur = visibleButtons.get(i); + cur.setNextFocusLeftId(i == 0 ? R.id.title : visibleButtons.get(i - 1).getId()); + cur.setNextFocusRightId(i == visibleButtons.size() - 1 ? R.id.title : visibleButtons.get(i + 1).getId()); + cur.setNextFocusDownId(R.id.typeRecycler); + } + mBinding.title.setNextFocusRightId(visibleButtons.get(0).getId()); + } } @Override @@ -660,23 +666,7 @@ public void onRefresh() { @Override public void reloadConfig() { - VodConfig.get().clear().config(getConfig()).load(new Callback() { - @Override - public void start() { - mBinding.progressLayout.showProgress(); - } - - @Override - public void success() { - showContent(); - } - - @Override - public void error(String msg) { - Notify.show(msg); - showContent(); - } - }); + initConfig(); } @Override diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java index 6c2bb6d07cb..f52d7035ea6 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SearchActivity.java @@ -25,7 +25,7 @@ import com.fongmi.android.tv.ui.base.BaseActivity; import com.fongmi.android.tv.ui.custom.CustomKeyboard; import com.fongmi.android.tv.ui.custom.CustomTextListener; -import com.fongmi.android.tv.ui.dialog.SiteDialog; +import com.fongmi.android.tv.ui.dialog.SearchSourceDialog; import com.fongmi.android.tv.utils.KeyUtil; import com.fongmi.android.tv.utils.SearchSuggest; import com.fongmi.android.tv.utils.Util; @@ -276,7 +276,7 @@ public void onSearch() { @Override public void showDialog() { - SiteDialog.create().search().show(this); + SearchSourceDialog.create().show(this); } @Override diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java index be6bedcbfe1..24f42c473cd 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingActivity.java @@ -54,6 +54,7 @@ public class SettingActivity extends BaseActivity implements ConfigListener, Sit private ActivitySettingBinding mBinding; private String[] size; private String[] language; + private String[] titleLines; public static void start(Activity activity) { activity.startActivity(new Intent(activity, SettingActivity.class)); @@ -94,6 +95,7 @@ private void setOtherText() { mBinding.incognitoText.setText(getSwitch(Setting.isIncognito())); mBinding.languageText.setText((language = ResUtil.getStringArray(R.array.select_language))[Setting.getLanguageIndex()]); mBinding.sizeText.setText((size = ResUtil.getStringArray(R.array.select_size))[PlayerSetting.getSize()]); + mBinding.titleLinesText.setText((titleLines = ResUtil.getStringArray(R.array.select_title_lines))[Setting.getTitleLinesIndex()]); } private void setCacheText() { @@ -113,6 +115,7 @@ protected void initEvent() { mBinding.wall.setOnClickListener(this::onWall); mBinding.size.setOnClickListener(this::setSize); mBinding.language.setOnClickListener(this::setLanguage); + mBinding.titleLines.setOnClickListener(this::setTitleLines); mBinding.cache.setOnClickListener(this::onCache); mBinding.backup.setOnClickListener(this::onBackup); mBinding.enhance.setOnClickListener(this::onEnhance); @@ -284,6 +287,13 @@ private void setLanguage(View view) { RefreshEvent.language(); } + private void setTitleLines(View view) { + int index = (Setting.getTitleLinesIndex() + 1) % titleLines.length; + mBinding.titleLinesText.setText(titleLines[index]); + Setting.putTitleLinesIndex(index); + RefreshEvent.size(); + } + private void setDoh(View view) { DohDialog.create().index(getDohIndex()).show(this); } @@ -360,4 +370,4 @@ private void setWallText() { mBinding.wallUrl.setText(Setting.getWallDesc(WallConfig.getDesc())); } -} +} \ No newline at end of file diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingEnhanceActivity.java b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingEnhanceActivity.java index d1b5e57d5f0..902f2dff661 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingEnhanceActivity.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/activity/SettingEnhanceActivity.java @@ -30,6 +30,7 @@ import com.fongmi.android.tv.ui.dialog.RemoteTrustDialog; import com.fongmi.android.tv.ui.dialog.ShellProxyDialog; import com.fongmi.android.tv.ui.dialog.SiteHealthDialog; +import com.fongmi.android.tv.ui.dialog.SourceBlockDialog; import com.fongmi.android.tv.ui.dialog.ViewingRecordSyncDialog; import com.fongmi.android.tv.ui.dialog.WebHomeExtensionDialog; import com.fongmi.android.tv.utils.LoginStateSync; @@ -40,7 +41,7 @@ public class SettingEnhanceActivity extends BaseActivity { - private static final String URL_GITHUB = "https://github.com/fish2018/webhtv"; + private static final String URL_GITHUB = "https://github.com/llb0/webhtv"; private static final String URL_CNB = "https://cnb.cool/fish2035/ext"; private ActivitySettingEnhanceBinding mBinding; @@ -69,6 +70,7 @@ protected void initView(Bundle savedInstanceState) { protected void initEvent() { mBinding.githubRepo.setOnClickListener(view -> openRepo(URL_GITHUB)); mBinding.cnbRepo.setOnClickListener(view -> openRepo(URL_CNB)); + mBinding.fileSites.setOnClickListener(this::setFileSites); mBinding.driveCheck.setOnClickListener(this::setDriveCheck); mBinding.debugLog.setOnClickListener(this::setDebugLog); mBinding.siteHealthSort.setOnClickListener(view -> SiteHealthDialog.show(this, this::setText)); @@ -97,6 +99,7 @@ protected void initEvent() { private void reorderItems() { ViewGroup parent = (ViewGroup) mBinding.customCsp.getParent(); View[] order = { + mBinding.fileSites, mBinding.customCsp, mBinding.webHomeExtension, mBinding.gitCloud, @@ -120,6 +123,7 @@ private void reorderItems() { private void setText() { if (!canSetText()) return; + safeSet("fileSites", mBinding.fileSitesText, () -> getString(R.string.setting_source_block_count, Setting.getSourceBlockedCount(), Setting.SOURCE_ALL.length)); safeSet("driveCheck", mBinding.driveCheckText, () -> getSwitch(Setting.isDriveCheck())); safeSet("debugLog", mBinding.debugLogText, () -> getSwitch(Setting.isDebugLog())); safeSet("siteHealthSort", mBinding.siteHealthSortText, () -> getSwitch(Setting.isSiteHealthSort())); @@ -195,6 +199,10 @@ private interface TextSupplier { CharSequence get(); } + private void setFileSites(View view) { + SourceBlockDialog.create(this::setText).show(this); + } + private void setDriveCheck(View view) { Setting.putDriveCheck(!Setting.isDriveCheck()); mBinding.driveCheckText.setText(getSwitch(Setting.isDriveCheck())); @@ -252,4 +260,4 @@ protected void onResume() { super.onResume(); setText(); } -} +} \ No newline at end of file diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java index 86c230ae28c..7cbcbe7c027 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java @@ -13,6 +13,7 @@ import com.fongmi.android.tv.App; import com.fongmi.android.tv.bean.Vod; import com.fongmi.android.tv.databinding.AdapterSearchBinding; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.utils.ImgUtil; import com.fongmi.android.tv.utils.ResUtil; @@ -116,7 +117,7 @@ public int getItemCount() { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { ViewHolder holder = new ViewHolder(AdapterSearchBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); holder.binding.getRoot().getLayoutParams().width = width; - holder.binding.getRoot().getLayoutParams().height = height + ResUtil.dp2px(34); + holder.binding.getRoot().getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; holder.binding.image.getLayoutParams().height = height; return holder; } @@ -147,19 +148,17 @@ public static class ViewHolder extends RecyclerView.ViewHolder { ViewHolder(@NonNull AdapterSearchBinding binding) { super(binding.getRoot()); this.binding = binding; - binding.name.setSingleLine(true); - binding.name.setHorizontallyScrolling(true); - binding.name.setMarqueeRepeatLimit(-1); binding.getRoot().setOnFocusChangeListener((view, hasFocus) -> setMarquee(hasFocus)); } private void bindName(String name) { + Setting.applyTitleMaxLines(binding.name); + binding.name.setHorizontallyScrolling(Setting.resolveTitleMaxLines() <= 1); binding.name.setText(name); setMarquee(binding.getRoot().hasFocus()); } private void setMarquee(boolean focused) { - binding.name.setEllipsize(focused ? TextUtils.TruncateAt.MARQUEE : TextUtils.TruncateAt.END); binding.name.setSelected(focused); } } diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SearchSourceAdapter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SearchSourceAdapter.java new file mode 100644 index 00000000000..684e526545e --- /dev/null +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SearchSourceAdapter.java @@ -0,0 +1,96 @@ +package com.fongmi.android.tv.ui.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fongmi.android.tv.databinding.ItemSearchSourceBinding; +import com.fongmi.android.tv.utils.SearchSourceItem; + +import java.util.ArrayList; +import java.util.List; + +public class SearchSourceAdapter extends RecyclerView.Adapter { + + private final List items = new ArrayList<>(); + private int checked = 0; + private OnConfigListener configListener; + private OnModeChangeListener modeChangeListener; + + public interface OnConfigListener { + void onConfig(SearchSourceItem item); + } + + public interface OnModeChangeListener { + void onModeChanged(int mode); + } + + public void setOnConfigListener(OnConfigListener l) { + this.configListener = l; + } + + public void setOnModeChangeListener(OnModeChangeListener l) { + this.modeChangeListener = l; + } + + public void setItems(List items, int checked) { + this.items.clear(); + this.items.addAll(items); + this.checked = checked; + notifyDataSetChanged(); + } + + public int getChecked() { + return checked; + } + + private int findPositionOfMode(int mode) { + for (int i = 0; i < items.size(); i++) if (items.get(i).mode == mode) return i; + return -1; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemSearchSourceBinding binding = ItemSearchSourceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + SearchSourceItem item = items.get(position); + holder.binding.title.setText(item.titleRes); + holder.binding.subtitle.setText(item.subtitle == null ? "" : item.subtitle); + holder.binding.subtitle.setVisibility(item.subtitle == null || item.subtitle.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE); + holder.binding.getRoot().setSelected(checked == item.mode); + holder.binding.config.setVisibility(item.configurable ? android.view.View.VISIBLE : android.view.View.GONE); + holder.binding.getRoot().setOnClickListener(v -> { + int prev = checked; + checked = item.mode; + int p1 = findPositionOfMode(prev); + int p2 = findPositionOfMode(checked); + if (p1 >= 0) notifyItemChanged(p1); + if (p2 >= 0) notifyItemChanged(p2); + if (modeChangeListener != null) modeChangeListener.onModeChanged(checked); + }); + holder.binding.config.setOnClickListener(v -> { + if (configListener != null) configListener.onConfig(item); + }); + } + + @Override + public int getItemCount() { + return items.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ItemSearchSourceBinding binding; + + ViewHolder(ItemSearchSourceBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java index a0d811827f6..0c31dcbb69d 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java @@ -49,6 +49,10 @@ public interface OnClickListener { void onItemClick(Site item); } + public interface OnDeleteListener { + void onDelete(Site item); + } + public void setType(int type) { int oldViewType = getViewType(); this.type = type; @@ -213,6 +217,10 @@ void bind(Site item) { actionBinding.check.setChecked(getChecked(item)); actionBinding.text.setSelected(item.isSelected()); actionBinding.getRoot().setSelected(item.isSelected()); + actionBinding.delete.setVisibility(item.isFile() && type != 0 ? android.view.View.VISIBLE : android.view.View.GONE); + actionBinding.delete.setOnClickListener(v -> { + if (listener instanceof OnDeleteListener) ((OnDeleteListener) listener).onDelete(item); + }); } else { switchBinding.text.setText(item.getName()); switchBinding.health.setBackgroundTintList(ColorStateList.valueOf(SiteHealthStore.getColor(item))); diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SiteCheckAdapter.java b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SiteCheckAdapter.java new file mode 100644 index 00000000000..9f917836e40 --- /dev/null +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/adapter/SiteCheckAdapter.java @@ -0,0 +1,111 @@ +package com.fongmi.android.tv.ui.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fongmi.android.tv.bean.Site; +import com.fongmi.android.tv.databinding.ItemSiteCheckBinding; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SiteCheckAdapter extends RecyclerView.Adapter { + + private List all = new ArrayList<>(); + private List filtered = new ArrayList<>(); + private final Set checkedKeys = new HashSet<>(); + + public void setItems(List sites, Set initial) { + all = new ArrayList<>(sites); + filtered = new ArrayList<>(sites); + checkedKeys.clear(); + if (initial != null) checkedKeys.addAll(initial); + notifyDataSetChanged(); + } + + public void filter(String keyword) { + if (keyword == null || keyword.isEmpty()) { + filtered = new ArrayList<>(all); + } else { + filtered = new ArrayList<>(); + String k = keyword.toLowerCase(); + for (Site s : all) { + if (s.getName().toLowerCase().contains(k) || s.getKey().toLowerCase().contains(k)) filtered.add(s); + } + } + notifyDataSetChanged(); + } + + public void selectAll() { + for (Site s : all) checkedKeys.add(s.getKey()); + notifyDataSetChanged(); + } + + public void selectNone() { + checkedKeys.clear(); + notifyDataSetChanged(); + } + + public void selectInvert() { + Set allKeys = new HashSet<>(); + for (Site s : all) allKeys.add(s.getKey()); + Set newChecked = new HashSet<>(allKeys); + newChecked.removeAll(checkedKeys); + checkedKeys.clear(); + checkedKeys.addAll(newChecked); + notifyDataSetChanged(); + } + + public Set getCheckedKeys() { + return new HashSet<>(checkedKeys); + } + + public int getCheckedCount() { + return checkedKeys.size(); + } + + public int getTotalCount() { + return all.size(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemSiteCheckBinding binding = ItemSiteCheckBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Site site = filtered.get(position); + holder.binding.check.setChecked(checkedKeys.contains(site.getKey())); + holder.binding.name.setText(site.getName()); + String typeText = site.getType() == 1 ? "JS" : site.getType() == 2 ? "PY" : "PQ"; + holder.binding.type.setText(typeText); + holder.binding.getRoot().setOnClickListener(v -> { + String key = site.getKey(); + if (checkedKeys.contains(key)) checkedKeys.remove(key); + else checkedKeys.add(key); + notifyItemChanged(position); + }); + } + + @Override + public int getItemCount() { + return filtered.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ItemSiteCheckBinding binding; + + ViewHolder(ItemSiteCheckBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomTitleView.java b/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomTitleView.java index 8ff0a1289e9..f1d66f42886 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomTitleView.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/custom/CustomTitleView.java @@ -49,7 +49,7 @@ public void setListener(Listener listener) { } private boolean hasEvent(KeyEvent event) { - return !getHome().isEmpty() && (KeyUtil.isLeftKey(event) || KeyUtil.isRightKey(event) || (KeyUtil.isUpKey(event) && !coolDown)); + return !getHome().isEmpty() && (KeyUtil.isUpKey(event) && !coolDown); } @Override @@ -68,8 +68,6 @@ public boolean dispatchKeyEvent(KeyEvent event) { private void onKeyDown(KeyEvent event) { if (KeyUtil.isActionDown(event) && KeyUtil.isUpKey(event)) onKeyUp(); - else if (KeyUtil.isActionDown(event) && KeyUtil.isLeftKey(event)) listener.setSite(getSite(false)); - else if (KeyUtil.isActionDown(event) && KeyUtil.isRightKey(event)) listener.setSite(getSite(true)); } private void onKeyUp() { diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SearchSitePickerDialog.java b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SearchSitePickerDialog.java new file mode 100644 index 00000000000..6a8b28af2c4 --- /dev/null +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SearchSitePickerDialog.java @@ -0,0 +1,123 @@ +package com.fongmi.android.tv.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import androidx.viewbinding.ViewBinding; + +import com.fongmi.android.tv.R; +import com.fongmi.android.tv.databinding.DialogSitePickerBinding; +import com.fongmi.android.tv.ui.adapter.SiteCheckAdapter; +import com.fongmi.android.tv.utils.ResUtil; +import com.fongmi.android.tv.utils.SearchSourceHelper; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class SearchSitePickerDialog extends BaseAlertDialog { + + private DialogSitePickerBinding binding; + private SiteCheckAdapter adapter; + private final List initial; + private final boolean tagMode; + private Callback callback; + + public interface Callback { + void onConfirm(List keys); + } + + public static SearchSitePickerDialog createSite(List initial, Callback callback) { + SearchSitePickerDialog d = new SearchSitePickerDialog(initial, false); + d.callback = callback; + return d; + } + + public static SearchSitePickerDialog createTag(List initial, Callback callback) { + SearchSitePickerDialog d = new SearchSitePickerDialog(initial, true); + d.callback = callback; + return d; + } + + private SearchSitePickerDialog(List initial, boolean tagMode) { + this.initial = initial == null ? new ArrayList<>() : new ArrayList<>(initial); + this.tagMode = tagMode; + } + + public void show(FragmentActivity activity) { + if (activity.isFinishing() || activity.isDestroyed()) return; + show(activity.getSupportFragmentManager(), null); + } + + @Override + protected ViewBinding getBinding() { + return binding = DialogSitePickerBinding.inflate(getLayoutInflater()); + } + + @Override + protected MaterialAlertDialogBuilder getBuilder() { + return builder().setView(getBinding().getRoot()); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + + @Override + protected void initView() { + setupVisibility(); + adapter = new SiteCheckAdapter(); + binding.recycler.setAdapter(adapter); + adapter.setItems(SearchSourceHelper.buildPickerSites(tagMode), new HashSet<>(initial)); + updateCount(); + } + + private void setupVisibility() { + boolean isSite = !tagMode; + binding.search.setVisibility(isSite ? android.view.View.VISIBLE : android.view.View.GONE); + binding.count.setVisibility(isSite ? android.view.View.VISIBLE : android.view.View.GONE); + } + + private void updateCount() { + if (binding.count.getVisibility() == android.view.View.VISIBLE) { + binding.count.setText(getString(R.string.search_site_count_of, adapter.getCheckedCount(), adapter.getTotalCount())); + } + } + + @Override + protected void initEvent() { + Window w = getDialog() != null ? getDialog().getWindow() : null; + if (w != null) { + WindowManager.LayoutParams p = w.getAttributes(); + p.width = (int) (ResUtil.getScreenWidth() * 0.7f); + w.setAttributes(p); + } + binding.search.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override public void afterTextChanged(Editable s) { + if (adapter != null) adapter.filter(s.toString()); + } + }); + binding.selectAll.setOnClickListener(v -> { adapter.selectAll(); updateCount(); }); + binding.selectNone.setOnClickListener(v -> { adapter.selectNone(); updateCount(); }); + binding.selectInvert.setOnClickListener(v -> { adapter.selectInvert(); updateCount(); }); + binding.confirm.setOnClickListener(v -> { + if (callback != null) callback.onConfirm(new ArrayList<>(adapter.getCheckedKeys())); + dismiss(); + }); + } +} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SearchSourceDialog.java b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SearchSourceDialog.java new file mode 100644 index 00000000000..28fbde396b1 --- /dev/null +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SearchSourceDialog.java @@ -0,0 +1,98 @@ +package com.fongmi.android.tv.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import androidx.viewbinding.ViewBinding; + +import com.fongmi.android.tv.databinding.DialogSearchSourceBinding; +import com.fongmi.android.tv.ui.adapter.SearchSourceAdapter; +import com.fongmi.android.tv.utils.ResUtil; +import com.fongmi.android.tv.utils.SearchModeStore; +import com.fongmi.android.tv.utils.SearchSourceHelper; +import com.fongmi.android.tv.utils.SearchSourceItem; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.List; + +public class SearchSourceDialog extends BaseAlertDialog { + + private DialogSearchSourceBinding binding; + private SearchSourceAdapter adapter; + + public static SearchSourceDialog create() { + return new SearchSourceDialog(); + } + + public void show(FragmentActivity activity) { + if (activity.isFinishing() || activity.isDestroyed()) return; + show(activity.getSupportFragmentManager(), null); + } + + @Override + protected ViewBinding getBinding() { + return binding = DialogSearchSourceBinding.inflate(getLayoutInflater()); + } + + @Override + protected MaterialAlertDialogBuilder getBuilder() { + return builder().setView(getBinding().getRoot()); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + + @Override + protected void initView() { + adapter = new SearchSourceAdapter(); + adapter.setItems(SearchSourceHelper.buildItems(), SearchModeStore.getMode()); + binding.recycler.setAdapter(adapter); + adapter.setOnConfigListener(this::openConfig); + adapter.setOnModeChangeListener(mode -> { + SearchModeStore.putMode(mode); + dismiss(); + }); + } + + @Override + protected void initEvent() { + Window w = getDialog() != null ? getDialog().getWindow() : null; + if (w != null) { + WindowManager.LayoutParams p = w.getAttributes(); + p.width = (int) (ResUtil.getScreenWidth() * 0.55f); + w.setAttributes(p); + } + } + + private void openConfig(SearchSourceItem item) { + int mode = item.mode; + if (mode == SearchModeStore.MODE_WHITE || mode == SearchModeStore.MODE_BLACK) { + List initial = mode == SearchModeStore.MODE_WHITE + ? SearchModeStore.getWhiteList() : SearchModeStore.getBlackList(); + SearchSitePickerDialog.createSite(new ArrayList<>(initial), selected -> { + if (mode == SearchModeStore.MODE_WHITE) SearchModeStore.putWhiteList(new ArrayList<>(selected)); + else SearchModeStore.putBlackList(new ArrayList<>(selected)); + SearchModeStore.putMode(mode); + dismiss(); + }).show(requireActivity()); + } else if (mode == SearchModeStore.MODE_TAG) { + SearchSitePickerDialog.createTag(new ArrayList<>(SearchModeStore.getTagGroups()), selected -> { + SearchModeStore.putTagGroups(new ArrayList<>(selected)); + SearchModeStore.putMode(mode); + dismiss(); + }).show(requireActivity()); + } + } +} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java index 974eabbe244..1b3d00aa09c 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java @@ -9,6 +9,8 @@ import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; +import android.widget.Toast; +import java.io.File; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -25,14 +27,16 @@ import com.fongmi.android.tv.databinding.DialogSiteBinding; import com.fongmi.android.tv.impl.Callback; import com.fongmi.android.tv.impl.SiteListener; +import com.fongmi.android.tv.R; import com.fongmi.android.tv.ui.adapter.SiteAdapter; import com.fongmi.android.tv.ui.custom.SpaceItemDecoration; import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.ResUtil; +import com.github.catvod.utils.Path; import com.github.catvod.crawler.SpiderDebug; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -public class SiteDialog extends BaseAlertDialog implements SiteAdapter.OnClickListener { +public class SiteDialog extends BaseAlertDialog implements SiteAdapter.OnClickListener, SiteAdapter.OnDeleteListener { private static final int GRID_COUNT = 3; private static final String TAG = "site_dialog"; @@ -273,6 +277,35 @@ public void onItemClick(Site item) { dismiss(); } + @Override + public void onDelete(Site item) { + if (!item.isFile()) return; + new MaterialAlertDialogBuilder(getDialogActivity()) + .setTitle(R.string.setting_site_delete_title) + .setMessage(getString(R.string.setting_site_delete_message, item.getName())) + .setPositiveButton(android.R.string.ok, (dialog, which) -> deleteFileSite(item)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void deleteFileSite(Site item) { + String type = item.getFileType(); + String fileName = item.getFileName(); + if (fileName.isEmpty()) return; + String subDir = switch (type) { + case "XBPQ" -> "sites-json"; + case "JS" -> "sites-js/api"; + case "PY" -> "sites-py"; + case "RAW" -> "sites"; + default -> ""; + }; + if (subDir.isEmpty()) return; + File file = new File(Path.root() + "/tvbox/" + subDir, fileName); + if (file.exists()) file.delete(); + if (activity != null) Toast.makeText(activity, getString(R.string.setting_site_delete_done, item.getName()), Toast.LENGTH_SHORT).show(); + dismiss(); + } + private void loadConfig(FragmentActivity activity, Config config) { if (config.getUrl().equals(VodConfig.getUrl())) return; VodConfig.load(config, new Callback() { diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SourceBlockDialog.java b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SourceBlockDialog.java new file mode 100644 index 00000000000..ab8c6ad7155 --- /dev/null +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/dialog/SourceBlockDialog.java @@ -0,0 +1,109 @@ +package com.fongmi.android.tv.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import androidx.viewbinding.ViewBinding; + +import com.fongmi.android.tv.R; +import com.fongmi.android.tv.databinding.DialogSitePickerBinding; +import com.fongmi.android.tv.setting.Setting; +import com.fongmi.android.tv.setting.SourceBlockItem; +import com.fongmi.android.tv.ui.adapter.SourceCheckAdapter; +import com.fongmi.android.tv.utils.ResUtil; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.List; + +public class SourceBlockDialog extends BaseAlertDialog { + + private DialogSitePickerBinding binding; + private SourceCheckAdapter adapter; + private Callback callback; + + public interface Callback { + void onConfirm(); + } + + public static SourceBlockDialog create(Callback callback) { + SourceBlockDialog d = new SourceBlockDialog(); + d.callback = callback; + return d; + } + + public void show(FragmentActivity activity) { + if (activity.isFinishing() || activity.isDestroyed()) return; + show(activity.getSupportFragmentManager(), null); + } + + @Override + protected ViewBinding getBinding() { + return binding = DialogSitePickerBinding.inflate(getLayoutInflater()); + } + + @Override + protected MaterialAlertDialogBuilder getBuilder() { + return builder().setTitle(R.string.setting_source_block_dialog_title).setView(getBinding().getRoot()); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + + @Override + protected void initView() { + binding.search.setVisibility(View.GONE); + binding.count.setVisibility(View.GONE); + adapter = new SourceCheckAdapter(); + binding.recycler.setAdapter(adapter); + adapter.setItems(buildSourceItems()); + } + + private List buildSourceItems() { + List items = new ArrayList<>(); + int[] sources = Setting.SOURCE_ALL; + String[] names = { + getString(R.string.source_vod_url), + getString(R.string.source_live_url), + getString(R.string.source_sites_json), + getString(R.string.source_sites_js), + getString(R.string.source_sites_py), + getString(R.string.source_sites_raw), + getString(R.string.source_lives_file) + }; + for (int i = 0; i < sources.length; i++) { + items.add(new SourceBlockItem(sources[i], names[i])); + } + return items; + } + + @Override + protected void initEvent() { + Window w = getDialog() != null ? getDialog().getWindow() : null; + if (w != null) { + WindowManager.LayoutParams p = w.getAttributes(); + p.width = (int) (ResUtil.getScreenWidth() * 0.7f); + w.setAttributes(p); + } + binding.selectAll.setOnClickListener(v -> adapter.selectAll()); + binding.selectNone.setOnClickListener(v -> adapter.selectNone()); + binding.selectInvert.setOnClickListener(v -> adapter.selectInvert()); + binding.confirm.setOnClickListener(v -> { + Setting.putSourceBlockMask(adapter.getMask()); + if (callback != null) callback.onConfirm(); + dismiss(); + }); + } +} diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodListHolder.java b/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodListHolder.java index b549fb6e84e..6e406a4ff62 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodListHolder.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodListHolder.java @@ -4,6 +4,7 @@ import com.bumptech.glide.Glide; import com.fongmi.android.tv.bean.Vod; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.databinding.AdapterVodListBinding; import com.fongmi.android.tv.ui.base.BaseVodHolder; import com.fongmi.android.tv.ui.presenter.VodPresenter; @@ -22,6 +23,7 @@ public VodListHolder(@NonNull AdapterVodListBinding binding, VodPresenter.OnClic @Override public void initView(Vod item) { + Setting.applyTitleMaxLines(binding.name); binding.name.setText(item.getName()); binding.remark.setText(item.getRemarks()); binding.name.setVisibility(item.getNameVisible()); @@ -35,4 +37,4 @@ public void initView(Vod item) { public void unbind() { Glide.with(binding.image).clear(binding.image); } -} +} \ No newline at end of file diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java b/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java index 8b083211212..b31108ed920 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java @@ -4,6 +4,7 @@ import com.bumptech.glide.Glide; import com.fongmi.android.tv.bean.Vod; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.databinding.AdapterVodOvalBinding; import com.fongmi.android.tv.ui.base.BaseVodHolder; import com.fongmi.android.tv.ui.presenter.VodPresenter; @@ -28,6 +29,7 @@ public VodOvalHolder size(int[] size) { @Override public void initView(Vod item) { + Setting.applyTitleMaxLines(binding.name); binding.name.setText(item.getName()); binding.name.setVisibility(item.getNameVisible()); binding.getRoot().setOnClickListener(v -> listener.onItemClick(item)); @@ -39,4 +41,4 @@ public void initView(Vod item) { public void unbind() { Glide.with(binding.image).clear(binding.image); } -} +} \ No newline at end of file diff --git a/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java b/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java index d28f13d2ed3..0e080192849 100644 --- a/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java +++ b/app/src/leanback/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java @@ -4,6 +4,7 @@ import com.bumptech.glide.Glide; import com.fongmi.android.tv.bean.Vod; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.databinding.AdapterVodRectBinding; import com.fongmi.android.tv.ui.base.BaseVodHolder; import com.fongmi.android.tv.ui.presenter.VodPresenter; @@ -28,6 +29,7 @@ public VodRectHolder size(int[] size) { @Override public void initView(Vod item) { + Setting.applyTitleMaxLines(binding.name); binding.name.setText(item.getName()); binding.year.setText(item.getYear()); binding.site.setText(item.getSiteName()); @@ -45,4 +47,4 @@ public void initView(Vod item) { public void unbind() { Glide.with(binding.image).clear(binding.image); } -} +} \ No newline at end of file diff --git a/app/src/leanback/res/drawable/ic_site_delete.xml b/app/src/leanback/res/drawable/ic_site_delete.xml new file mode 100644 index 00000000000..75a9cae5c6b --- /dev/null +++ b/app/src/leanback/res/drawable/ic_site_delete.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/leanback/res/layout/activity_home.xml b/app/src/leanback/res/layout/activity_home.xml index 2dafd49f525..8445ae06cb4 100644 --- a/app/src/leanback/res/layout/activity_home.xml +++ b/app/src/leanback/res/layout/activity_home.xml @@ -86,14 +86,80 @@ android:layout_marginEnd="16dp" android:layout_weight="1" android:ellipsize="marquee" - android:focusable="false" + android:focusable="true" + android:focusableInTouchMode="true" android:nextFocusDown="@id/typeRecycler" android:singleLine="true" android:text="@string/app_name" android:textColor="@color/white" android:textSize="24sp" /> - + + + + + + + + + + + + + + - + android:layout_weight="1" + android:background="@drawable/selector_item" + android:focusable="true" + android:focusableInTouchMode="true" + android:orientation="horizontal"> + + + + - + + + android:background="@drawable/selector_item" + android:focusable="true" + android:focusableInTouchMode="true" + android:orientation="horizontal"> + + + + + - + \ No newline at end of file diff --git a/app/src/leanback/res/layout/activity_setting_enhance.xml b/app/src/leanback/res/layout/activity_setting_enhance.xml index 9a27c5e1df6..f5e8ffa295e 100644 --- a/app/src/leanback/res/layout/activity_setting_enhance.xml +++ b/app/src/leanback/res/layout/activity_setting_enhance.xml @@ -56,6 +56,35 @@ + + + + + + + + diff --git a/app/src/leanback/res/layout/adapter_site.xml b/app/src/leanback/res/layout/adapter_site.xml index 941f748e870..4a42bbf4d00 100644 --- a/app/src/leanback/res/layout/adapter_site.xml +++ b/app/src/leanback/res/layout/adapter_site.xml @@ -38,4 +38,15 @@ android:focusableInTouchMode="false" app:buttonTint="@color/white" /> - + + + \ No newline at end of file diff --git a/app/src/leanback/res/layout/adapter_vod_list.xml b/app/src/leanback/res/layout/adapter_vod_list.xml index 608cca4b612..f3d3200e873 100644 --- a/app/src/leanback/res/layout/adapter_vod_list.xml +++ b/app/src/leanback/res/layout/adapter_vod_list.xml @@ -33,7 +33,6 @@ android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" - android:singleLine="true" android:textColor="@color/white" android:textSize="16sp" tools:text="蜘蛛人" /> diff --git a/app/src/leanback/res/layout/adapter_vod_oval.xml b/app/src/leanback/res/layout/adapter_vod_oval.xml index f059d22608c..3068dc6f18d 100644 --- a/app/src/leanback/res/layout/adapter_vod_oval.xml +++ b/app/src/leanback/res/layout/adapter_vod_oval.xml @@ -23,9 +23,8 @@ android:layout_alignStart="@+id/image" android:layout_alignEnd="@+id/image" android:layout_marginTop="4dp" - android:ellipsize="marquee" + android:ellipsize="end" android:gravity="center" - android:singleLine="true" android:textColor="@color/white" android:textSize="16sp" tools:text="蜘蛛人" /> diff --git a/app/src/leanback/res/layout/adapter_vod_rect.xml b/app/src/leanback/res/layout/adapter_vod_rect.xml index 3847c133953..cad6e78c194 100644 --- a/app/src/leanback/res/layout/adapter_vod_rect.xml +++ b/app/src/leanback/res/layout/adapter_vod_rect.xml @@ -59,10 +59,9 @@ android:layout_alignStart="@+id/image" android:layout_alignEnd="@+id/image" android:background="@drawable/shape_vod_name" - android:ellipsize="marquee" + android:ellipsize="end" android:gravity="center" android:padding="6dp" - android:singleLine="true" android:textColor="@color/white" android:textSize="16sp" tools:text="蜘蛛人" /> diff --git a/app/src/leanback/res/layout/dialog_search_source.xml b/app/src/leanback/res/layout/dialog_search_source.xml new file mode 100644 index 00000000000..83b97cd4baf --- /dev/null +++ b/app/src/leanback/res/layout/dialog_search_source.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/leanback/res/layout/dialog_site_picker.xml b/app/src/leanback/res/layout/dialog_site_picker.xml new file mode 100644 index 00000000000..d5e8866facd --- /dev/null +++ b/app/src/leanback/res/layout/dialog_site_picker.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/leanback/res/layout/item_search_source.xml b/app/src/leanback/res/layout/item_search_source.xml new file mode 100644 index 00000000000..bd6b3faac87 --- /dev/null +++ b/app/src/leanback/res/layout/item_search_source.xml @@ -0,0 +1,47 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/leanback/res/layout/item_site_check.xml b/app/src/leanback/res/layout/item_site_check.xml new file mode 100644 index 00000000000..4585d201038 --- /dev/null +++ b/app/src/leanback/res/layout/item_site_check.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/leanback/res/values-zh-rCN/strings.xml b/app/src/leanback/res/values-zh-rCN/strings.xml index f418b633502..3d816cb5339 100644 --- a/app/src/leanback/res/values-zh-rCN/strings.xml +++ b/app/src/leanback/res/values-zh-rCN/strings.xml @@ -1,6 +1,7 @@ + 刷新 点播 直播 搜索 @@ -18,7 +19,7 @@ 更新推荐 - %s”的搜索结果 + "%s"的搜索结果 推送剪贴板内容 @@ -33,4 +34,4 @@ 1 小时 延长 5 分钟 取消定时器 - + \ No newline at end of file diff --git a/app/src/leanback/res/values-zh-rTW/strings.xml b/app/src/leanback/res/values-zh-rTW/strings.xml index 596d07ea900..34566815b2c 100644 --- a/app/src/leanback/res/values-zh-rTW/strings.xml +++ b/app/src/leanback/res/values-zh-rTW/strings.xml @@ -1,6 +1,7 @@ + 刷新 點播 直播 搜尋 @@ -33,4 +34,4 @@ 1 小時 延長 5 分鐘 取消定時器 - + \ No newline at end of file diff --git a/app/src/leanback/res/values/strings.xml b/app/src/leanback/res/values/strings.xml index db2f7e9ca15..11d8639bdf2 100644 --- a/app/src/leanback/res/values/strings.xml +++ b/app/src/leanback/res/values/strings.xml @@ -1,6 +1,7 @@ + Refresh Vod Live Search @@ -33,4 +34,9 @@ 1 hour Delay 5 minutes Cancel timer - + + + Delete file site + Delete %1$s and its source file? This cannot be undone. + Deleted %1$s + \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/Startup.java b/app/src/main/java/com/fongmi/android/tv/Startup.java index 39de55c954d..a445c779a18 100644 --- a/app/src/main/java/com/fongmi/android/tv/Startup.java +++ b/app/src/main/java/com/fongmi/android/tv/Startup.java @@ -1,6 +1,8 @@ package com.fongmi.android.tv; import android.content.Context; +import android.os.Build; +import android.provider.Settings; import androidx.annotation.NonNull; import androidx.startup.Initializer; @@ -30,12 +32,19 @@ public Void create(@NonNull Context context) { Logger.addLogAdapter(new AndroidLogAdapter(PrettyFormatStrategy.newBuilder().methodCount(0).showThreadInfo(false).tag("TV").build())); EventBus.builder().addIndex(new EventIndex()).installDefaultEventBus(); OkHttp.dns().setDoh(Doh.objectFrom(Setting.getDoh())); + pinOverlayPermission(context); return null; } + private void pinOverlayPermission(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Settings.canDrawOverlays(context); + } + } + @NonNull @Override public List>> dependencies() { return Collections.emptyList(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/api/Decoder.java b/app/src/main/java/com/fongmi/android/tv/api/Decoder.java index dab772baa52..c5f862b4594 100644 --- a/app/src/main/java/com/fongmi/android/tv/api/Decoder.java +++ b/app/src/main/java/com/fongmi/android/tv/api/Decoder.java @@ -5,8 +5,10 @@ import com.fongmi.android.tv.utils.UrlUtil; import com.github.catvod.net.OkHttp; import com.github.catvod.utils.Json; +import com.github.catvod.utils.Path; import com.github.catvod.utils.Util; +import java.io.File; import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -23,6 +25,11 @@ public class Decoder { private static final Pattern JS_URI = Pattern.compile("\"(\\.|\\.\\.)/(.?|.+?)\\.js\\?(.?|.+?)\""); public static String getJson(String url, String tag) throws Exception { + File local = UrlUtil.toLocalFile(url); + if (local != null) { + if (!local.exists()) throw new Exception("Config file not found: " + url); + return verify(url, Path.read(local)); + } try (Response res = OkHttp.newCall(url, tag).execute()) { HttpUrl httpUrl = res.request().url(); int size = HttpUrl.parse(url).querySize(); @@ -83,4 +90,4 @@ private static String extract(String data) { private static String padEnd(String key) { return key + "0000000000000000".substring(key.length()); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/api/config/LiveConfig.java b/app/src/main/java/com/fongmi/android/tv/api/config/LiveConfig.java index 552ef303c1f..8b1fe09658b 100644 --- a/app/src/main/java/com/fongmi/android/tv/api/config/LiveConfig.java +++ b/app/src/main/java/com/fongmi/android/tv/api/config/LiveConfig.java @@ -18,13 +18,17 @@ import com.fongmi.android.tv.impl.Callback; import com.fongmi.android.tv.setting.CustomCspSetting; import com.fongmi.android.tv.setting.LiveSetting; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.utils.UrlUtil; import com.github.catvod.bean.Header; import com.github.catvod.bean.Proxy; import com.github.catvod.utils.Json; +import com.github.catvod.utils.Path; import com.google.gson.JsonObject; +import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -70,7 +74,12 @@ public static boolean isEmpty() { } public static boolean hasLoadedLives() { - return !get().getLives().isEmpty(); + if (!get().getLives().isEmpty()) return true; + if (!Setting.isSourceAllowed(Setting.SOURCE_LIVES_FILE)) return false; + File dir = new File(Path.root() + "/tvbox/lives/"); + if (!dir.exists() || !dir.isDirectory()) return false; + File[] files = dir.listFiles(f -> f.isFile() && !f.getName().startsWith(".")); + return files != null && files.length > 0; } public static boolean hasUrl() { @@ -120,12 +129,19 @@ protected void postEvent() { @Override protected void load(Config config) throws Throwable { if (config.isEmpty()) { - initLive(config, new JsonObject()); + parseConfig(config, new JsonObject()); return; } - String json = Decoder.getJson(UrlUtil.convert(config.getUrl()), TAG); - if (Json.isObj(json)) checkJson(config, Json.parse(json).getAsJsonObject()); - else parseText(config, json); + try { + String json = Decoder.getJson(UrlUtil.convert(config.getUrl()), TAG); + if (Json.isObj(json)) { + checkJson(config, Json.parse(json).getAsJsonObject()); + } else { + parseText(config, json); + } + return; + } catch (Throwable ignored) {} + parseConfig(config, new JsonObject()); } @Override @@ -151,8 +167,18 @@ public void load() { } private void parseText(Config config, String text) { - Live live = new Live(UrlUtil.getName(config.getUrl()), config.getUrl()).sync(); - lives = new ArrayList<>(List.of(live)); + initList(new JsonObject()); + List lives = new ArrayList<>(); + if (Setting.isSourceAllowed(Setting.SOURCE_LIVE_URL)) { + Live live = new Live(UrlUtil.getName(config.getUrl()), config.getUrl()).sync(); + lives.add(live); + } + if (Setting.isSourceAllowed(Setting.SOURCE_LIVES_FILE)) { + lives.addAll(loadFileLives()); + } + setLives(lives); + Live live = lives.isEmpty() ? new Live() : lives.get(0); + if (live.isEmpty()) return; LiveParser.text(live, text); finishLive(config, ""); } @@ -197,16 +223,56 @@ private void initList(JsonObject object) { private void initLive(Config config, JsonObject object) { String spider = Json.safeString(object, "spider"); BaseLoader.get().parseJar(spider, false); - setLives(Json.safeListElement(object, "lives").stream().map(e -> Live.objectFrom(e, spider)).distinct().collect(Collectors.toCollection(ArrayList::new))); + List lives = new ArrayList<>(); + if (Setting.isSourceAllowed(Setting.SOURCE_LIVE_URL)) { + lives.addAll(Json.safeListElement(object, "lives").stream().map(e -> Live.objectFrom(e, spider)).distinct().collect(Collectors.toCollection(ArrayList::new))); + } + if (Setting.isSourceAllowed(Setting.SOURCE_LIVES_FILE)) { + lives.addAll(0, loadFileLives()); + } + setLives(lives); finishLive(config, spider); } + // ==================== 文件直播源加载器 ==================== + + private static final String CLAN_LIVE_ROOT = Path.root() + "/tvbox/lives/"; + + private List loadFileLives() { + List result = new ArrayList<>(); + if (!Setting.isSourceAllowed(Setting.SOURCE_LIVES_FILE)) return result; + File dir = new File(CLAN_LIVE_ROOT); + if (!dir.exists() || !dir.isDirectory()) return result; + File[] files = dir.listFiles(f -> f.isFile() && !f.getName().startsWith(".")); + if (files == null || files.length == 0) return result; + Arrays.sort(files, (a, b) -> a.getName().compareToIgnoreCase(b.getName())); + for (File file : files) { + try { + String name = file.getName(); + int dot = name.lastIndexOf('.'); + if (dot > 0) name = name.substring(0, dot); + if (name.isEmpty()) continue; + Live live = new Live(name, UrlUtil.convert("clan://lives/" + file.getName())); + LiveParser.start(live); + if (!live.getGroups().isEmpty()) result.add(live); + } catch (Throwable ignored) {} + } + return result; + } + private void finishLive(Config config, String spider) { CustomCspSetting.inject(getLives(), spider); getLives().removeIf(Live::isEmpty); Map items = Live.findAll().stream().collect(Collectors.toMap(Live::getName, Function.identity())); getLives().forEach(live -> live.sync(items.get(live.getName()))); setHome(config, getLives().isEmpty() ? new Live() : getLives().stream().filter(item -> item.getName().equals(config.getHome())).findFirst().orElse(getLives().get(0)), false); + if (getLives().isEmpty() && Setting.isSourceAllowed(Setting.SOURCE_LIVES_FILE)) { + List files = loadFileLives(); + if (!files.isEmpty()) { + setLives(files); + setHome(config, files.stream().filter(item -> item.getName().equals(config.getHome())).findFirst().orElse(files.get(0)), false); + } + } } public void setKeep(Channel channel) { @@ -296,4 +362,4 @@ private void setHome(Config config, Live live, boolean save) { private static class Loader { static volatile LiveConfig INSTANCE = new LiveConfig(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/api/config/VodConfig.java b/app/src/main/java/com/fongmi/android/tv/api/config/VodConfig.java index 8f911f80e16..36520c83ce9 100644 --- a/app/src/main/java/com/fongmi/android/tv/api/config/VodConfig.java +++ b/app/src/main/java/com/fongmi/android/tv/api/config/VodConfig.java @@ -11,19 +11,26 @@ import com.fongmi.android.tv.bean.Parse; import com.fongmi.android.tv.bean.Rule; import com.fongmi.android.tv.bean.Site; +import com.fongmi.android.tv.bean.Style; import com.fongmi.android.tv.event.ConfigEvent; import com.fongmi.android.tv.event.RefreshEvent; import com.fongmi.android.tv.impl.Callback; import com.fongmi.android.tv.setting.CustomCspSetting; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.utils.UrlUtil; import com.fongmi.android.tv.web.ext.WebHomeExtensionRegistry; import com.github.catvod.bean.Doh; import com.github.catvod.bean.Header; import com.github.catvod.bean.Proxy; import com.github.catvod.utils.Json; +import com.github.catvod.utils.Path; +import com.github.catvod.utils.Util; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -73,7 +80,7 @@ public static void load(Config config, Callback callback) { } public VodConfig init() { - return config(Config.vod()); + return clear().config(Config.vod()); } public VodConfig config(Config config) { @@ -115,8 +122,23 @@ protected void postEvent() { @Override protected void load(Config config) throws Throwable { - String json = Decoder.getJson(UrlUtil.convert(config.getUrl()), TAG); - checkJson(config, Json.parse(json).getAsJsonObject()); + if (config.isEmpty()) { + try { + initSites(config, "", new JsonObject()); + } catch (Throwable ignored) {} + return; + } + String globalSpider = ""; + try { + String json = Decoder.getJson(UrlUtil.convert(config.getUrl()), TAG); + JsonObject object = Json.parse(json).getAsJsonObject(); + globalSpider = Json.safeString(object, "spider"); + checkJson(config, object); + return; + } catch (Throwable ignored) {} + try { + initSites(config, globalSpider, new JsonObject()); + } catch (Throwable ignored) {} } @Override @@ -192,9 +214,19 @@ private void initWall(Config config, JsonObject object) { } private void initSite(Config config, JsonObject object) { - String spider = Json.safeString(object, "spider"); + initSites(config, "", object); + } + + private void initSites(Config config, String globalSpider, JsonObject object) { + String spider = TextUtils.isEmpty(globalSpider) ? Json.safeString(object, "spider") : globalSpider; BaseLoader.get().parseJar(spider, true); - setSites(Json.safeListElement(object, "sites").stream().map(e -> Site.objectFrom(e, spider)).distinct().collect(Collectors.toCollection(ArrayList::new))); + List sites = new ArrayList<>(); + if (Setting.isSourceAllowed(Setting.SOURCE_VOD_URL)) { + sites.addAll(Json.safeListElement(object, "sites").stream().map(e -> Site.objectFrom(e, spider)).distinct().collect(Collectors.toCollection(ArrayList::new))); + } + List fileSites = loadFileSites(spider); + sites.addAll(0, fileSites); + setSites(sites); Map items = Site.findAll().stream().collect(Collectors.toMap(Site::getKey, Function.identity())); getSites().forEach(site -> site.sync(items.get(site.getKey()))); CustomCspSetting.Result custom = CustomCspSetting.inject(getSites()); @@ -309,6 +341,191 @@ private void setParse(Config config, Parse parse, boolean save) { if (save) config.save(); } + // ==================== 文件站点加载器 ==================== + + private static final String CLAN_ROOT = Path.root() + "/tvbox/"; + private static final String XBPQ_JAR = CLAN_ROOT + "jars/XBPQ.jar"; + + private List loadFileSites(String globalSpider) { + List result = new ArrayList<>(); + if (Setting.isSourceAllowed(Setting.SOURCE_SITES_JSON)) { + try { result.addAll(loadXbpqSites(globalSpider)); } catch (Throwable ignored) {} + } + if (Setting.isSourceAllowed(Setting.SOURCE_SITES_JS)) { + try { result.addAll(loadJsSites(globalSpider)); } catch (Throwable ignored) {} + } + if (Setting.isSourceAllowed(Setting.SOURCE_SITES_PY)) { + try { result.addAll(loadPySites(globalSpider)); } catch (Throwable ignored) {} + } + if (Setting.isSourceAllowed(Setting.SOURCE_SITES_RAW)) { + try { result.addAll(loadRawSites(globalSpider)); } catch (Throwable ignored) {} + } + return result; + } + + private List loadXbpqSites(String globalSpider) { + File dir = new File(CLAN_ROOT + "sites-json"); + List result = new ArrayList<>(); + if (!dir.exists() || !dir.isDirectory()) return result; + List files = listSorted(dir); + if (files.isEmpty()) return result; + String jar; + if (!TextUtils.isEmpty(globalSpider) + && globalSpider.toLowerCase().contains("xbpq") + && Path.local(globalSpider) != null + && Path.local(globalSpider).exists()) { + jar = globalSpider; + } else { + jar = XBPQ_JAR; + BaseLoader.get().parseJar(jar, true); + } + for (File file : files) { + FileMeta meta = parseFileMeta(file.getName()); + if (meta.name.isEmpty()) continue; + Site site = Site.get("XBPQ_" + file.getName() + "_file", meta.name); + site.setType(3); + site.setApi("csp_XBPQ"); + site.setExt(UrlUtil.convert("clan://sites-json/" + file.getName())); + site.setJar(jar); + site.setName(meta.name + " | PQ"); + meta.apply(site); + result.add(site); + } + return result; + } + + private List loadJsSites(String globalSpider) { + File dir = new File(CLAN_ROOT + "sites-js", "api"); + List result = new ArrayList<>(); + if (!dir.exists() || !dir.isDirectory()) return result; + List files = listSorted(dir); + for (File file : files) { + FileMeta meta = parseFileMeta(file.getName()); + if (meta.name.isEmpty()) continue; + Site site = Site.get("JS_" + file.getName() + "_file", meta.name); + site.setType(3); + site.setApi(UrlUtil.convert("clan://sites-js/api/" + file.getName())); + site.setJar(globalSpider); + site.setName(meta.name + " | JS"); + meta.apply(site); + result.add(site); + } + return result; + } + + private List loadPySites(String globalSpider) { + File dir = new File(CLAN_ROOT + "sites-py"); + List result = new ArrayList<>(); + if (!dir.exists() || !dir.isDirectory()) return result; + List files = listSorted(dir); + for (File file : files) { + FileMeta meta = parseFileMeta(file.getName()); + if (meta.name.isEmpty()) continue; + Site site = Site.get("PY_" + file.getName() + "_file", meta.name); + site.setType(3); + site.setApi(UrlUtil.convert("clan://sites-py/" + file.getName())); + site.setJar(globalSpider); + site.setName(meta.name + " | PY"); + meta.apply(site); + result.add(site); + } + return result; + } + + private List loadRawSites(String globalSpider) { + File dir = new File(CLAN_ROOT + "sites"); + List result = new ArrayList<>(); + if (!dir.exists() || !dir.isDirectory()) return result; + List files = listSorted(dir); + for (File file : files) { + FileMeta meta = parseFileMeta(file.getName()); + String content = Path.read(file); + if (content.isEmpty()) continue; + try { + JsonElement element = Json.parse(content); + if (element.isJsonObject()) { + Site site = Site.objectFrom(element, globalSpider); + if (site.getName().isEmpty()) site.setName(meta.name); + site.setKey("RAW_" + file.getName() + "_file"); + meta.apply(site); + result.add(site); + } + } catch (Throwable ignored) {} + } + return result; + } + + private List listSorted(File dir) { + File[] files = dir.listFiles(f -> f.isFile() && !f.getName().startsWith(".")); + if (files == null) return Collections.emptyList(); + Arrays.sort(files, (a, b) -> { + FileMeta ma = parseFileMeta(a.getName()); + FileMeta mb = parseFileMeta(b.getName()); + int c = Integer.compare(ma.order, mb.order); + if (c != 0) return c; + return ma.name.compareToIgnoreCase(mb.name); + }); + return Arrays.asList(files); + } + + private FileMeta parseFileMeta(String fileName) { + FileMeta meta = new FileMeta(); + if (fileName == null) return meta; + + String stem = fileName; + int lastDot = fileName.lastIndexOf('.'); + if (lastDot > 0) stem = fileName.substring(0, lastDot); + + if (stem.matches("^\\d+_.*")) { + int sep = stem.indexOf('_'); + meta.order = Integer.parseInt(stem.substring(0, sep)); + stem = stem.substring(sep + 1); + } + + int firstDot = stem.indexOf('.'); + if (firstDot > 0) { + String func = stem.substring(firstDot + 1); + String upper = func.toUpperCase(); + + boolean isFunc = upper.startsWith("N") || upper.startsWith("S") || func.contains("-"); + + if (isFunc) { + stem = stem.substring(0, firstDot); + + if (upper.startsWith("N")) { meta.searchable = 0; meta.quickSearch = 0; } + else if (upper.startsWith("S")) { meta.hide = 1; } + + int dash = func.lastIndexOf('-'); + if (dash >= 0 && dash < func.length() - 1) { + String tail = func.substring(dash + 1); + if (tail.equalsIgnoreCase("H")) meta.ratio = 1.33f; + else if (tail.equalsIgnoreCase("S")) meta.ratio = 1.0f; + else { try { meta.ratio = Float.parseFloat(tail); } + catch (NumberFormatException ignored) {} } + } + } + } + + meta.name = stem.trim(); + return meta; + } + + private static class FileMeta { + int order = Integer.MAX_VALUE; + String name = ""; + Integer searchable; + Integer quickSearch; + Integer hide; + float ratio = 0; + + void apply(Site site) { + if (searchable != null) site.setSearchable(searchable); + if (quickSearch != null) site.setQuickSearch(quickSearch); + if (hide != null) site.setHide(hide); + if (ratio > 0) site.setStyle(new Style("rect", ratio)); + } + } + private void setHome(Config config, Site site, boolean save) { home = site; home.setSelected(true); @@ -320,4 +537,4 @@ private void setHome(Config config, Site site, boolean save) { private static class Loader { static volatile VodConfig INSTANCE = new VodConfig(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/api/loader/BaseLoader.java b/app/src/main/java/com/fongmi/android/tv/api/loader/BaseLoader.java index fedd49fb422..fe5a11bc016 100644 --- a/app/src/main/java/com/fongmi/android/tv/api/loader/BaseLoader.java +++ b/app/src/main/java/com/fongmi/android/tv/api/loader/BaseLoader.java @@ -48,11 +48,9 @@ private static boolean isCsp(String api) { } public void clear() { - Task.execute(() -> { - jarLoader.clear(); - pyLoader.clear(); - jsLoader.clear(); - }); + jarLoader.clear(); + pyLoader.clear(); + jsLoader.clear(); } public Spider getSpider(String key, String api, String ext, String jar) { diff --git a/app/src/main/java/com/fongmi/android/tv/api/loader/JarLoader.java b/app/src/main/java/com/fongmi/android/tv/api/loader/JarLoader.java index 1c22f032271..036ff6f12fe 100644 --- a/app/src/main/java/com/fongmi/android/tv/api/loader/JarLoader.java +++ b/app/src/main/java/com/fongmi/android/tv/api/loader/JarLoader.java @@ -67,10 +67,6 @@ private void load(String key, File file) { SpiderDebug.log("jar-loader", "load skip missing key=%s file=%s", key, file); return; } - if (!file.setReadOnly()) { - SpiderDebug.log("jar-loader", "load skip readonly failed key=%s file=%s size=%s", key, file.getAbsolutePath(), file.length()); - return; - } String cachePath = Path.jar().getAbsolutePath(); SpiderDebug.log("jar-loader", "load start key=%s file=%s size=%s cache=%s", key, file.getAbsolutePath(), file.length(), cachePath); DexClassLoader loader = new CspDexClassLoader(file.getAbsolutePath(), cachePath, cachePath, App.get().getClassLoader()); @@ -111,10 +107,9 @@ private void invokeNetworkCompat(String key, DexClassLoader loader) { Throwable cause = e; while (cause.getCause() != null) cause = cause.getCause(); SpiderDebug.log("youtube-iframe", "probe failed error=%s", cause.getClass().getName() + ":" + cause.getMessage()); - SpiderDebug.log("youtube-iframe", cause); + SpiderDebug.log("jar-loader", cause); } } catch (ClassNotFoundException | NoSuchMethodException ignored) { - // Optional compatibility hook used only by jars which expose it. } catch (Throwable e) { SpiderDebug.log("jar-loader", "network client inject failed key=%s error=%s", key, error(e)); } @@ -159,12 +154,19 @@ public void parseJar(String key, String jar) { if (md5.startsWith("http")) md5 = OkHttp.string(md5).trim(); jar = texts[0]; SpiderDebug.log("jar-loader", "parse start key=%s source=%s md5=%s", key, source(jar), !md5.isEmpty()); - if (!md5.isEmpty() && Util.equals(jar, md5)) { - load(key, Path.jar(jar)); - } else if (jar.startsWith("http")) { - load(key, Download.create(jar, Path.jar(jar)).get()); - } else if (jar.startsWith("file")) { - load(key, Path.local(jar)); + try { + if (!md5.isEmpty() && Util.equals(jar, md5)) { + load(key, Path.jar(jar)); + } else if (jar.startsWith("http")) { + load(key, Download.create(jar, Path.jar(jar)).get()); + } else if (jar.startsWith("file")) { + load(key, Path.local(jar)); + } else { + File local = UrlUtil.toLocalFile(jar); + if (local != null && local.exists()) load(key, local); + } + } catch (Throwable e) { + SpiderDebug.log("jar-loader", "parse error key=%s jar=%s error=%s", key, jar, e.getMessage()); } } } @@ -208,8 +210,11 @@ public Spider getSpider(String key, String api, String ext, String jar) { } private String source(String jar) { + if (jar == null) return null; + jar = jar.replace("clan://", "file://tvbox/"); if (jar.startsWith("http")) return "http:" + Util.md5(jar); if (jar.startsWith("file")) return "file:" + jar.length(); + if (jar.startsWith("/")) return "local:" + jar; return jar; } diff --git a/app/src/main/java/com/fongmi/android/tv/bean/Backup.java b/app/src/main/java/com/fongmi/android/tv/bean/Backup.java index ec065d6d8c2..5d2d4f52463 100644 --- a/app/src/main/java/com/fongmi/android/tv/bean/Backup.java +++ b/app/src/main/java/com/fongmi/android/tv/bean/Backup.java @@ -146,12 +146,10 @@ private Map restoreConfig() { Map cids = new HashMap<>(); for (Config item : getConfig()) { int source = item.getId(); - Config current = AppDatabase.get().getConfigDao().find(item.getUrl(), item.getType()); - item.setId(current == null ? 0 : current.getId()); + AppDatabase.get().getConfigDao().delete(item.getUrl(), item.getType()); + item.setId(0); long id = AppDatabase.get().getConfigDao().insert(item); - if (id == -1) AppDatabase.get().getConfigDao().update(item); - else item.setId(Math.toIntExact(id)); - if (source > 0) cids.put(source, item.getId()); + if (source > 0) cids.put(source, Math.toIntExact(id)); } return cids; } diff --git a/app/src/main/java/com/fongmi/android/tv/bean/Config.java b/app/src/main/java/com/fongmi/android/tv/bean/Config.java index 3310a07e084..d51caf31a17 100644 --- a/app/src/main/java/com/fongmi/android/tv/bean/Config.java +++ b/app/src/main/java/com/fongmi/android/tv/bean/Config.java @@ -249,21 +249,22 @@ public String getDesc() { } public Config insert() { - if (isEmpty()) return this; setId(Math.toIntExact(AppDatabase.get().getConfigDao().insert(this))); return this; } public Config save() { - if (isEmpty()) return this; - AppDatabase.get().getConfigDao().insertOrUpdate(this); + setTime(System.currentTimeMillis()); + if (id == 0) { + setId(Math.toIntExact(AppDatabase.get().getConfigDao().insert(this))); + } else { + AppDatabase.get().getConfigDao().update(this); + } + Prefers.put("config_" + getType(), getUrl()); return this; } public Config update() { - if (isEmpty()) return this; - setTime(System.currentTimeMillis()); - Prefers.put("config_" + getType(), getUrl()); return save(); } @@ -285,4 +286,4 @@ public boolean equals(@Nullable Object obj) { if (!(obj instanceof Config it)) return false; return getId() == it.getId(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/bean/Site.java b/app/src/main/java/com/fongmi/android/tv/bean/Site.java index cc95f511652..9e7d5e25705 100644 --- a/app/src/main/java/com/fongmi/android/tv/bean/Site.java +++ b/app/src/main/java/com/fongmi/android/tv/bean/Site.java @@ -375,6 +375,24 @@ public void setSelected(Site item) { this.selected = item.equals(this); } + public boolean isFile() { + return getKey().endsWith("_file"); + } + + public String getFileName() { + if (!isFile()) return ""; + String raw = getKey().substring(0, getKey().length() - 5); + int idx = raw.indexOf('_'); + return idx >= 0 ? raw.substring(idx + 1) : raw; + } + + public String getFileType() { + if (!isFile()) return ""; + String raw = getKey().substring(0, getKey().length() - 5); + int idx = raw.indexOf('_'); + return idx >= 0 ? raw.substring(0, idx) : raw; + } + public boolean isHide() { return getHide() == 1; } diff --git a/app/src/main/java/com/fongmi/android/tv/player/PlayerHelper.java b/app/src/main/java/com/fongmi/android/tv/player/PlayerHelper.java index 8ca17942433..3c9d2f53814 100644 --- a/app/src/main/java/com/fongmi/android/tv/player/PlayerHelper.java +++ b/app/src/main/java/com/fongmi/android/tv/player/PlayerHelper.java @@ -88,6 +88,7 @@ public static void share(Activity activity, String url, Map head public static void choose(Activity activity, String url, Map headers, boolean isVod, long position, CharSequence title) { try { if (url == null || url.isEmpty()) return; + url = url.replace("clan://", "file://tvbox/"); List list = new ArrayList<>(); headers.forEach((key, value) -> { list.add(key); diff --git a/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsController.java b/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsController.java index a16177c7900..482dda17c17 100644 --- a/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsController.java +++ b/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsController.java @@ -399,6 +399,8 @@ public static boolean isMusicLikeText(String text) { || lower.contains("聽歌") || lower.contains("听歌") || lower.contains("有声") || lower.contains("有聲") || lower.contains("听书") || lower.contains("聽書") + || lower.contains("[听]") || lower.contains("[聽]") + || lower.contains("【听】") || lower.contains("【聽】") || lower.contains("audiobook") || lower.contains("audio book") || lower.contains("podcast") || lower.contains("播客") || lower.contains("电台") || lower.contains("電台") || lower.contains("radio"); diff --git a/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRepository.java b/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRepository.java index 757eee56067..8d406cf8b46 100644 --- a/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRepository.java +++ b/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRepository.java @@ -755,6 +755,7 @@ private String fileBase(String name) { private File sourceFile(String url) { try { if (TextUtils.isEmpty(url)) return null; + url = url.replace("clan://", "file://tvbox/"); if (url.startsWith("file://")) return new File(Uri.parse(url).getPath()); if (url.startsWith("/")) return new File(url); return null; diff --git a/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRequest.java b/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRequest.java index c4157c4bb94..4efa0c99d43 100644 --- a/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRequest.java +++ b/app/src/main/java/com/fongmi/android/tv/player/lyrics/LyricsRequest.java @@ -407,6 +407,7 @@ private static String cleanArtist(String text) { private static String cleanTitle(String title, String url) { String value = clean(title); + value = value.replace("clan://", "file://tvbox/"); if (TextUtils.isEmpty(value) || value.startsWith("http://") || value.startsWith("https://") || value.startsWith("file://")) value = fileName(url); if (TextUtils.isEmpty(value)) value = fileName(title); return stripExtension(value); diff --git a/app/src/main/java/com/fongmi/android/tv/server/process/DebugLogs.java b/app/src/main/java/com/fongmi/android/tv/server/process/DebugLogs.java index acfbeff917d..3426b9e3e39 100644 --- a/app/src/main/java/com/fongmi/android/tv/server/process/DebugLogs.java +++ b/app/src/main/java/com/fongmi/android/tv/server/process/DebugLogs.java @@ -134,7 +134,7 @@ private String scriptEnhanced() { + "function proxyName(s){return between(s,'proxy=[',']').replace('SOCKS @ ','SOCKS ').replace('/','')}" + "function parse(line){const a=line.indexOf(' ['),b=line.indexOf('] ',a+2),c=line.indexOf(': ',b+2);return{line,time:a>0?line.slice(0,a):'',thread:a>0&&b>0?line.slice(a+2,b):'',tag:b>0&&c>0?line.slice(b+2,c):'',msg:c>0?line.slice(c+2):line}}" + "function base(r){return{kind:'raw',state:'raw',badge:r.tag||'日志',title:r.tag||'原始日志',detail:r.msg||r.line,raw:r.line,time:r.time}}" - + "function explain(r){const text=(r.tag+': '+r.msg),low=text.toLowerCase();let e=base(r);if(r.tag==='webview-console'||r.tag==='webhome-console'){e.kind='console';e.state=(low.includes('error')||low.includes('exception')||low.includes('uncaught'))?'err':(low.includes('warning')||low.includes('warn'))?'warn':'ok';e.badge='Console';e.title=r.tag==='webhome-console'?'WebHome 控制台输出':'网页控制台输出';e.detail=r.msg;return e}if(r.tag==='server'){e.kind='server';e.state='raw';e.badge='服务';e.title='App 本机 HTTP 服务收到请求';e.detail=r.msg;return e}if(low.includes('error')||low.includes('exception')||low.includes('failed')||low.includes('timeout')||low.includes('失败')||low.includes('崩溃')||low.includes('异常')){e.kind='error';e.state='err';e.badge='错误';e.title='发现错误或异常';return e}" + + "function explain(r){const text=(r.tag+': '+r.msg),low=text.toLowerCase();let e=base(r);if(r.tag==='webview-console'||r.tag==='webhome-console'||r.tag==='quickjs'||r.tag==='python-spider'){e.kind='console';e.state=(low.includes('error')||low.includes('exception')||low.includes('uncaught'))?'err':(low.includes('warning')||low.includes('warn'))?'warn':'ok';e.badge='Console';e.title=r.tag==='webhome-console'?'WebHome 控制台输出':r.tag==='quickjs'?'QuickJS 控制台输出':r.tag==='python-spider'?'Python 控制台输出':'网页控制台输出';e.detail=r.msg;return e}if(r.tag==='server'){e.kind='server';e.state='raw';e.badge='服务';e.title='App 本机 HTTP 服务收到请求';e.detail=r.msg;return e}if(low.includes('error')||low.includes('exception')||low.includes('failed')||low.includes('timeout')||low.includes('失败')||low.includes('崩溃')||low.includes('异常')){e.kind='error';e.state='err';e.badge='错误';e.title='发现错误或异常';return e}" + "if(r.tag==='startup'){e.kind='startup';e.state='ok';e.badge='启动';e.title='启动阶段耗时';e.detail=r.msg;return e}" + "if(r.tag==='debug'){e.kind='server';e.state='ok';e.badge='调试';e.title=r.msg.includes('ready')?'调试日志服务已准备':'调试日志状态变化';e.detail=r.msg;return e}" + "if(r.tag==='env'){e.kind='startup';e.state='ok';e.badge='环境';e.title='设备和系统环境';e.detail=r.msg;return e}" diff --git a/app/src/main/java/com/fongmi/android/tv/setting/CustomCspSetting.java b/app/src/main/java/com/fongmi/android/tv/setting/CustomCspSetting.java index cd7441bde60..ae1f26f46df 100644 --- a/app/src/main/java/com/fongmi/android/tv/setting/CustomCspSetting.java +++ b/app/src/main/java/com/fongmi/android/tv/setting/CustomCspSetting.java @@ -230,6 +230,7 @@ private static boolean hasLocalLiveMethod(String api) { if (TextUtils.isEmpty(api)) return false; try { String value = api.trim(); + value = value.replace("clan://", "file://tvbox/"); String path; if (value.startsWith("file://")) path = value.substring("file://".length()); else { diff --git a/app/src/main/java/com/fongmi/android/tv/setting/PreloadSetting.java b/app/src/main/java/com/fongmi/android/tv/setting/PreloadSetting.java index 6a18953fd2e..f3b9064b4de 100644 --- a/app/src/main/java/com/fongmi/android/tv/setting/PreloadSetting.java +++ b/app/src/main/java/com/fongmi/android/tv/setting/PreloadSetting.java @@ -6,7 +6,7 @@ public class PreloadSetting { public static final int MAX_THREADS = 4; public static final int DEFAULT_THREADS = 1; public static final int MIN_SIZE_MB = 128; - public static final int MAX_SIZE_MB = 32768; + public static final int MAX_SIZE_MB = 4096; public static final int MIN_TIME_SECONDS = 20; public static final int MAX_TIME_SECONDS = 120; public static final int DEFAULT_TIME_SECONDS = 20; @@ -18,7 +18,7 @@ public class PreloadSetting { public static final int PAUSE_PRELOAD_WIFI = 1; public static final int PAUSE_PRELOAD_ALWAYS = 2; public static final int DEFAULT_PAUSE_PRELOAD = PAUSE_PRELOAD_ALWAYS; - private static final int[] SIZE_OPTIONS_MB = {128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768}; + private static final int[] SIZE_OPTIONS_MB = {128, 256, 512, 1024, 2048, 4096}; private static final int[] AHEAD_OPTIONS_SECONDS = {60, 180, 300, 600, 1800, 3600, WHOLE_MEDIA_AHEAD_SECONDS}; private static final int[] PAUSE_PRELOAD_OPTIONS = {PAUSE_PRELOAD_ALWAYS, PAUSE_PRELOAD_WIFI}; diff --git a/app/src/main/java/com/fongmi/android/tv/setting/ProxySetting.java b/app/src/main/java/com/fongmi/android/tv/setting/ProxySetting.java index f84f9dc53c1..c2a81987be5 100644 --- a/app/src/main/java/com/fongmi/android/tv/setting/ProxySetting.java +++ b/app/src/main/java/com/fongmi/android/tv/setting/ProxySetting.java @@ -118,6 +118,7 @@ private static void addLocalFileUrls(String source, Set urls) { private static String localPath(String source) { String value = source == null ? "" : source.trim(); if (TextUtils.isEmpty(value)) return ""; + value = value.replace("clan://", "file://tvbox/"); if (value.startsWith("file://")) return value.substring("file://".length()); Uri uri = Uri.parse(value); String path = uri.getPath(); diff --git a/app/src/main/java/com/fongmi/android/tv/setting/Setting.java b/app/src/main/java/com/fongmi/android/tv/setting/Setting.java index c6b725c3454..1fc4829bd18 100644 --- a/app/src/main/java/com/fongmi/android/tv/setting/Setting.java +++ b/app/src/main/java/com/fongmi/android/tv/setting/Setting.java @@ -46,12 +46,28 @@ public class Setting { public static final int CSP_WARMUP_CUSTOM = 2; public static final int UI_SCALE_FOLLOW_SYSTEM = 0; + public static final int UI_SCALE_MILD_RELAXED = 6; public static final int UI_SCALE_STANDARD = 1; public static final int UI_SCALE_COMPACT = 2; public static final int UI_SCALE_SMALLER = 3; public static final int UI_SCALE_MILD_COMPACT = 4; public static final int UI_SCALE_MORE_COMPACT = 5; - private static final int[] UI_SCALE_OPTIONS = {UI_SCALE_FOLLOW_SYSTEM, UI_SCALE_STANDARD, UI_SCALE_MILD_COMPACT, UI_SCALE_COMPACT, UI_SCALE_MORE_COMPACT, UI_SCALE_SMALLER}; + private static final int[] UI_SCALE_OPTIONS = {UI_SCALE_FOLLOW_SYSTEM, UI_SCALE_MILD_RELAXED, UI_SCALE_STANDARD, UI_SCALE_MILD_COMPACT, UI_SCALE_COMPACT, UI_SCALE_MORE_COMPACT, UI_SCALE_SMALLER}; + + public static final int TITLE_LINES_OFF = 0; + public static final int TITLE_LINES_2 = 1; + public static final int TITLE_LINES_3 = 2; + public static final int TITLE_LINES_UNLIMITED = 3; + private static final int[] TITLE_LINES_OPTIONS = {TITLE_LINES_OFF, TITLE_LINES_2, TITLE_LINES_3, TITLE_LINES_UNLIMITED}; + + public static final int SOURCE_VOD_URL = 1 << 0; + public static final int SOURCE_LIVE_URL = 1 << 1; + public static final int SOURCE_SITES_JSON = 1 << 2; + public static final int SOURCE_SITES_JS = 1 << 3; + public static final int SOURCE_SITES_PY = 1 << 4; + public static final int SOURCE_SITES_RAW = 1 << 5; + public static final int SOURCE_LIVES_FILE = 1 << 6; + public static final int[] SOURCE_ALL = {SOURCE_VOD_URL, SOURCE_LIVE_URL, SOURCE_SITES_JSON, SOURCE_SITES_JS, SOURCE_SITES_PY, SOURCE_SITES_RAW, SOURCE_LIVES_FILE}; public static final int WALL_CINEMA = 5; public static final int WALL_CINEMA_WARM = 6; @@ -303,7 +319,7 @@ public static void putSyncMode(int mode) { } public static String getSyncPaths() { - return Prefers.getString("sync_paths", "TV\nTVBox\nTVData"); + return Prefers.getString("sync_paths", ""); } public static void putSyncPaths(String paths) { @@ -350,6 +366,14 @@ public static void putIncognito(boolean incognito) { Prefers.put("incognito", incognito); } + public static boolean isFileSites() { + return Prefers.getBoolean("file_sites", true); + } + + public static void putFileSites(boolean fileSites) { + Prefers.put("file_sites", fileSites); + } + public static int getLanguage() { int language = Prefers.getInt("language", LANGUAGE_FOLLOW_SYSTEM); return isLanguage(language) ? language : LANGUAGE_FOLLOW_SYSTEM; @@ -402,7 +426,7 @@ public static Context wrapLanguage(Context context) { } public static int getUiScale() { - int scale = Prefers.getInt("ui_scale", UI_SCALE_FOLLOW_SYSTEM); + int scale = Prefers.getInt("ui_scale", UI_SCALE_MILD_RELAXED); return isUiScale(scale) ? scale : UI_SCALE_FOLLOW_SYSTEM; } @@ -443,6 +467,7 @@ public static Context wrapUiScale(Context context) { private static float getUiScaleFactor(int scale) { return switch (scale) { + case UI_SCALE_MILD_RELAXED -> 0.9f; case UI_SCALE_STANDARD -> 0.8f; case UI_SCALE_MILD_COMPACT -> 0.75f; case UI_SCALE_COMPACT -> 0.7f; @@ -456,6 +481,52 @@ private static int pxToDp(int px, int densityDpi) { return Math.max(1, Math.round(px * (float) DisplayMetrics.DENSITY_DEFAULT / densityDpi)); } + public static int getTitleLines() { + int lines = Prefers.getInt("title_lines", TITLE_LINES_UNLIMITED); + return isTitleLines(lines) ? lines : TITLE_LINES_OFF; + } + + public static void putTitleLines(int lines) { + Prefers.put("title_lines", isTitleLines(lines) ? lines : TITLE_LINES_OFF); + } + + public static int getTitleLinesIndex() { + int lines = getTitleLines(); + for (int i = 0; i < TITLE_LINES_OPTIONS.length; i++) if (TITLE_LINES_OPTIONS[i] == lines) return i; + return 0; + } + + public static void putTitleLinesIndex(int index) { + putTitleLines(index >= 0 && index < TITLE_LINES_OPTIONS.length ? TITLE_LINES_OPTIONS[index] : TITLE_LINES_OFF); + } + + private static boolean isTitleLines(int lines) { + for (int option : TITLE_LINES_OPTIONS) if (option == lines) return true; + return false; + } + + public static int resolveTitleMaxLines() { + return switch (getTitleLines()) { + case TITLE_LINES_2 -> 2; + case TITLE_LINES_3 -> 3; + case TITLE_LINES_UNLIMITED -> Integer.MAX_VALUE; + default -> 1; + }; + } + + public static void applyTitleMaxLines(android.widget.TextView textView) { + int maxLines = resolveTitleMaxLines(); + if (maxLines <= 1) { + textView.setSingleLine(true); + textView.setEllipsize(android.text.TextUtils.TruncateAt.MARQUEE); + textView.setMarqueeRepeatLimit(-1); + } else { + textView.setSingleLine(false); + textView.setMaxLines(maxLines); + textView.setEllipsize(android.text.TextUtils.TruncateAt.END); + } + } + public static boolean isDriveCheck() { return Prefers.getBoolean("drive_check", true); } @@ -708,4 +779,33 @@ public static boolean hasFileManager() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false; return new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + App.get().getPackageName())).resolveActivity(App.get().getPackageManager()) != null || new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION).resolveActivity(App.get().getPackageManager()) != null; } + public static int getSourceBlockMask() { + return Prefers.getInt("source_block_mask", 0); + } + + public static void putSourceBlockMask(int mask) { + Prefers.put("source_block_mask", mask); + } + + public static boolean isSourceBlocked(int source) { + return (getSourceBlockMask() & source) != 0; + } + + public static boolean isSourceAllowed(int source) { + return !isSourceBlocked(source); + } + + public static void setSourceBlocked(int source, boolean blocked) { + int mask = getSourceBlockMask(); + if (blocked) mask |= source; + else mask &= ~source; + putSourceBlockMask(mask); + } + + public static int getSourceBlockedCount() { + int mask = getSourceBlockMask(); + int count = 0; + for (int source : SOURCE_ALL) if ((mask & source) != 0) count++; + return count; + } } diff --git a/app/src/main/java/com/fongmi/android/tv/setting/SiteOrderStore.java b/app/src/main/java/com/fongmi/android/tv/setting/SiteOrderStore.java index 43b0e6955c7..efcc72fe18a 100644 --- a/app/src/main/java/com/fongmi/android/tv/setting/SiteOrderStore.java +++ b/app/src/main/java/com/fongmi/android/tv/setting/SiteOrderStore.java @@ -25,7 +25,11 @@ public static void sortSites(List sites) { if (keys.isEmpty()) return; Map indexes = new HashMap<>(); for (int i = 0; i < keys.size(); i++) indexes.put(keys.get(i), i); - sites.sort((a, b) -> Integer.compare(indexes.getOrDefault(a.getKey(), Integer.MAX_VALUE), indexes.getOrDefault(b.getKey(), Integer.MAX_VALUE))); + sites.sort((a, b) -> { + int ai = indexes.getOrDefault(a.getKey(), a.isFile() ? -1 : Integer.MAX_VALUE); + int bi = indexes.getOrDefault(b.getKey(), b.isFile() ? -1 : Integer.MAX_VALUE); + return Integer.compare(ai, bi); + }); } public static void save(List sites) { @@ -50,4 +54,4 @@ private static List load() { private static String key() { return KEY_PREFIX + VodConfig.getCid(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/setting/SourceBlockItem.java b/app/src/main/java/com/fongmi/android/tv/setting/SourceBlockItem.java new file mode 100644 index 00000000000..53143cae055 --- /dev/null +++ b/app/src/main/java/com/fongmi/android/tv/setting/SourceBlockItem.java @@ -0,0 +1,10 @@ +package com.fongmi.android.tv.setting; + +public class SourceBlockItem { + public final int source; + public final String name; + public SourceBlockItem(int source, String name) { + this.source = source; + this.name = name; + } +} diff --git a/app/src/main/java/com/fongmi/android/tv/ui/adapter/SourceCheckAdapter.java b/app/src/main/java/com/fongmi/android/tv/ui/adapter/SourceCheckAdapter.java new file mode 100644 index 00000000000..638f79b5ad1 --- /dev/null +++ b/app/src/main/java/com/fongmi/android/tv/ui/adapter/SourceCheckAdapter.java @@ -0,0 +1,100 @@ +package com.fongmi.android.tv.ui.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fongmi.android.tv.databinding.ItemSiteCheckBinding; +import com.fongmi.android.tv.setting.Setting; +import com.fongmi.android.tv.setting.SourceBlockItem; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SourceCheckAdapter extends RecyclerView.Adapter { + + private final List all = new ArrayList<>(); + private final Set blockedSources = new HashSet<>(); + + public void setItems(List items) { + all.clear(); + all.addAll(items); + blockedSources.clear(); + int mask = Setting.getSourceBlockMask(); + for (SourceBlockItem item : items) { + if ((mask & item.source) != 0) blockedSources.add(item.source); + } + notifyDataSetChanged(); + } + + public void selectAll() { + for (SourceBlockItem item : all) blockedSources.add(item.source); + notifyDataSetChanged(); + } + + public void selectNone() { + blockedSources.clear(); + notifyDataSetChanged(); + } + + public void selectInvert() { + Set newBlocked = new HashSet<>(); + for (SourceBlockItem item : all) { + if (!blockedSources.contains(item.source)) newBlocked.add(item.source); + } + blockedSources.clear(); + blockedSources.addAll(newBlocked); + notifyDataSetChanged(); + } + + public int getBlockedCount() { + return blockedSources.size(); + } + + public int getTotalCount() { + return all.size(); + } + + public int getMask() { + int mask = 0; + for (int src : blockedSources) mask |= src; + return mask; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemSiteCheckBinding binding = ItemSiteCheckBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + SourceBlockItem item = all.get(position); + holder.binding.check.setChecked(blockedSources.contains(item.source)); + holder.binding.name.setText(item.name); + holder.binding.type.setVisibility(android.view.View.GONE); + holder.binding.getRoot().setOnClickListener(v -> { + if (blockedSources.contains(item.source)) blockedSources.remove(item.source); + else blockedSources.add(item.source); + notifyItemChanged(position); + }); + } + + @Override + public int getItemCount() { + return all.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ItemSiteCheckBinding binding; + ViewHolder(ItemSiteCheckBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/main/java/com/fongmi/android/tv/ui/dialog/SyncPathDialog.java b/app/src/main/java/com/fongmi/android/tv/ui/dialog/SyncPathDialog.java index b7df9affcfe..383025c7dd1 100644 --- a/app/src/main/java/com/fongmi/android/tv/ui/dialog/SyncPathDialog.java +++ b/app/src/main/java/com/fongmi/android/tv/ui/dialog/SyncPathDialog.java @@ -98,22 +98,28 @@ private void appendChildren(String path, int depth) { root = Path.root().getCanonicalFile(); dir = path.isEmpty() ? root : new File(root, path).getCanonicalFile(); if (!inside(root, dir) || !dir.isDirectory()) return; - File[] files = dir.listFiles(file -> file.isDirectory() && !file.getName().startsWith(".")); - if (files != null) Arrays.sort(files, (a, b) -> a.getName().compareToIgnoreCase(b.getName())); + File[] files = dir.listFiles(file -> !file.getName().startsWith(".")); + if (files != null) { + Arrays.sort(files, (a, b) -> { + if (a.isDirectory() != b.isDirectory()) return a.isDirectory() ? -1 : 1; + return a.getName().compareToIgnoreCase(b.getName()); + }); + } int count = 0; for (File file : files == null ? new File[0] : files) { if (count++ >= TREE_LIMIT) break; String child = relativeTo(root, file); if (child.isEmpty()) continue; - Item item = new Item(file.getName(), child, depth, hasChildren(file)); + boolean isDir = file.isDirectory(); + Item item = new Item(file.getName(), child, depth, isDir && hasSubDirs(file), !isDir); items.add(item); - if (expanded.contains(child)) appendChildren(child, depth + 1); + if (isDir && expanded.contains(child)) appendChildren(child, depth + 1); } } catch (Exception ignored) { } } - private boolean hasChildren(File dir) { + private boolean hasSubDirs(File dir) { File[] files = dir.listFiles(file -> file.isDirectory() && !file.getName().startsWith(".")); return files != null && files.length > 0; } @@ -127,7 +133,6 @@ private void collapseAll() { private void reset() { selected.clear(); - selected.addAll(SyncFiles.getPaths(SyncFiles.DEFAULT_PATHS)); collapseAll(); } @@ -188,12 +193,14 @@ private static class Item { private final String path; private final int depth; private final boolean hasChildren; + private final boolean isFile; - private Item(String name, String path, int depth, boolean hasChildren) { + private Item(String name, String path, int depth, boolean hasChildren, boolean isFile) { this.name = name; this.path = path; this.depth = depth; this.hasChildren = hasChildren; + this.isFile = isFile; } } @@ -231,18 +238,19 @@ private void bind(Item item) { binding.name.setText(item.name); binding.path.setText(item.path); binding.check.setChecked(selected.contains(item.path)); + binding.icon.setImageResource(item.isFile ? R.drawable.ic_file : R.drawable.ic_folder); + binding.enter.setVisibility(item.isFile ? View.INVISIBLE : (item.hasChildren ? View.VISIBLE : View.INVISIBLE)); + binding.enter.setImageResource(expanded.contains(item.path) ? R.drawable.ic_detail_minus : R.drawable.ic_detail_plus); + binding.enter.setOnClickListener(v -> { + if (item.hasChildren) toggleExpanded(item.path); + }); binding.getRoot().setOnClickListener(v -> toggle(item.path)); binding.getRoot().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() != KeyEvent.ACTION_DOWN || keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) return false; - if (item.hasChildren) toggleExpanded(item.path); + if (!item.isFile && item.hasChildren) toggleExpanded(item.path); return true; }); - binding.enter.setVisibility(item.hasChildren ? View.VISIBLE : View.INVISIBLE); - binding.enter.setImageResource(expanded.contains(item.path) ? R.drawable.ic_detail_minus : R.drawable.ic_detail_plus); - binding.enter.setOnClickListener(v -> { - if (item.hasChildren) toggleExpanded(item.path); - }); } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/utils/AppBackup.java b/app/src/main/java/com/fongmi/android/tv/utils/AppBackup.java index 1a49a75b6d6..c76d81751ed 100644 --- a/app/src/main/java/com/fongmi/android/tv/utils/AppBackup.java +++ b/app/src/main/java/com/fongmi/android/tv/utils/AppBackup.java @@ -89,58 +89,36 @@ public static CreateResult create(File target) throws IOException { } public static synchronized CreateResult create(File target, Progress progress) throws IOException { - SyncFiles.Archive shared = null; LoginStateSync.Archive login = null; try { notifyProgress(progress, "整理数据库和设置", 5, 0, 0); Backup backup = Backup.create(); if (backup.getConfig().isEmpty()) throw new IOException("没有可备份的接口配置"); - notifyProgress(progress, "整理共享数据文件", 15, 0, 0); - StringBuilder warning = new StringBuilder(); - int customCspSourceFiles = 0; - int customCspFiles = 0; - try { - customCspSourceFiles = SyncFiles.countFiles(SyncFiles.CUSTOM_CSP_PATH); - shared = SyncFiles.createArchive(SyncFiles.getPaths(SyncFiles.DEFAULT_PATHS)); - customCspFiles = SyncFiles.countArchiveFiles(shared == null ? null : shared.getFile(), SyncFiles.CUSTOM_CSP_PATH); - if (customCspFiles < customCspSourceFiles) appendWarning(warning, "站点注入文件未完整写入备份"); - } catch (Throwable e) { - if (shared != null) shared.delete(); - shared = null; - appendWarning(warning, "共享数据文件未写入备份"); - SpiderDebug.log("backup", "shared archive warning error=%s", e.getMessage()); - } - notifyProgress(progress, "整理登录态和云盘凭据", 30, shared == null ? 0 : shared.getZipSize(), 0); + notifyProgress(progress, "整理登录态和云盘凭据", 30, 0, 0); try { login = LoginStateSync.createArchive(); } catch (Throwable e) { login = null; - appendWarning(warning, "登录态和云盘凭据未写入备份"); - SpiderDebug.log("backup", "login archive warning error=%s", e.getMessage()); } - String warningText = warning.toString(); byte[] data = backup.toString().getBytes(StandardCharsets.UTF_8); - byte[] manifest = manifest(shared, login, customCspSourceFiles, customCspFiles, backup.getWebHomeExtensionPreferenceCount(), backup.getWebHomeExtensionSourceCount(), warningText).getBytes(StandardCharsets.UTF_8); + byte[] manifest = manifest(null, login, 0, 0, backup.getWebHomeExtensionPreferenceCount(), backup.getWebHomeExtensionSourceCount(), "").getBytes(StandardCharsets.UTF_8); long total = data.length + manifest.length + appFilesSize(Path.files(), Path.files()); - if (shared != null) total += shared.getFile().length(); if (login != null) total += login.getFile().length(); Counter counter = new Counter(total, progress); try (ZipOutputStream output = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(Path.create(target)), BUFFER_SIZE))) { addBytes(output, DATA, data, counter); addBytes(output, MANIFEST, manifest, counter); - if (shared != null) addFile(output, SHARED, shared.getFile(), counter); if (login != null) addFile(output, LOGIN, login.getFile(), counter); addAppFiles(output, Path.files(), Path.files(), counter); } - notifyProgress(progress, warningText.isEmpty() ? "备份完成" : "备份完成,但部分数据未写入", 100, target.length(), target.length()); - SpiderDebug.log("backup", "create complete file=%s size=%d warning=%s", target.getAbsolutePath(), target.length(), warningText); - return new CreateResult(warningText); + notifyProgress(progress, "备份完成", 100, target.length(), target.length()); + SpiderDebug.log("backup", "create complete file=%s size=%d", target.getAbsolutePath(), target.length()); + return new CreateResult(""); } catch (Throwable e) { Path.clear(target); if (e instanceof IOException) throw (IOException) e; throw new IOException(e); } finally { - if (shared != null) shared.delete(); if (login != null) login.delete(); } } @@ -153,7 +131,6 @@ public static RestoreResult restore(File source, Progress progress) throws IOExc long sourceSize = source == null ? 0 : source.length(); notifyProgress(progress, "读取备份文件", 5, sourceSize, sourceSize); if (!isZip(source)) return restoreLegacy(source, progress); - File shared = null; File login = null; String json = ""; String manifestJson = ""; @@ -172,8 +149,6 @@ public static RestoreResult restore(File source, Progress progress) throws IOExc json = new String(readEntry(input, buffer), StandardCharsets.UTF_8); } else if (MANIFEST.equals(name)) { manifestJson = new String(readEntry(input, buffer), StandardCharsets.UTF_8); - } else if (SHARED.equals(name)) { - shared = extractTemp(input, "webhtv-shared-restore-", buffer); } else if (LOGIN.equals(name)) { login = extractTemp(input, "webhtv-login-restore-", buffer); } else if (name.startsWith(APP_FILES)) { @@ -191,7 +166,6 @@ public static RestoreResult restore(File source, Progress progress) throws IOExc } notifyProgress(progress, "校验备份内容", 45, sourceSize, sourceSize); } catch (Throwable e) { - Path.clear(shared); Path.clear(login); if (e instanceof IOException) throw (IOException) e; throw new IOException(e); @@ -199,17 +173,9 @@ public static RestoreResult restore(File source, Progress progress) throws IOExc Backup backup = Backup.objectFrom(json); try { if (backup.getConfig().isEmpty()) throw new IOException("备份缺少接口配置数据"); - String warning = validateManifest(manifestJson, shared, backup); + String warning = validateManifest(manifestJson, null, backup); StringBuilder restoreWarning = new StringBuilder(warning); - notifyProgress(progress, "恢复共享数据文件", 55, sourceSize, sourceSize); - int sharedFiles = 0; - try { - sharedFiles = shared == null ? 0 : SyncFiles.restoreArchive(shared); - } catch (Throwable e) { - appendWarning(restoreWarning, "共享数据文件未能完整恢复"); - SpiderDebug.log("backup", "shared restore warning error=%s", e.getMessage()); - } - notifyProgress(progress, "恢复登录态和云盘凭据", 70, sourceSize, sourceSize); + notifyProgress(progress, "恢复登录态和云盘凭据", 55, sourceSize, sourceSize); int loginFiles = 0; try { loginFiles = login == null ? 0 : LoginStateSync.restoreArchive(login); @@ -217,16 +183,15 @@ public static RestoreResult restore(File source, Progress progress) throws IOExc appendWarning(restoreWarning, "登录态和云盘凭据未能完整恢复"); SpiderDebug.log("backup", "login restore warning error=%s", e.getMessage()); } - notifyProgress(progress, "恢复数据库和设置", 85, sourceSize, sourceSize); + notifyProgress(progress, "恢复数据库和设置", 70, sourceSize, sourceSize); backup.restore(true); reload(); if (restoreWarning.length() > 0) restoreWarning.append(",其余可用数据已恢复"); String warningText = restoreWarning.toString(); notifyProgress(progress, warningText.isEmpty() ? "恢复完成" : "恢复完成,但部分数据缺失", 100, sourceSize, sourceSize); - SpiderDebug.log("backup", "restore complete shared=%d login=%d app=%d warning=%s", sharedFiles, loginFiles, appFiles, warningText); - return new RestoreResult(sharedFiles, loginFiles, appFiles, false, warningText); + SpiderDebug.log("backup", "restore complete login=%d app=%d warning=%s", loginFiles, appFiles, warningText); + return new RestoreResult(0, loginFiles, appFiles, false, warningText); } finally { - Path.clear(shared); Path.clear(login); } } diff --git a/app/src/main/java/com/fongmi/android/tv/utils/ConfigDownload.java b/app/src/main/java/com/fongmi/android/tv/utils/ConfigDownload.java new file mode 100644 index 00000000000..6282183dde2 --- /dev/null +++ b/app/src/main/java/com/fongmi/android/tv/utils/ConfigDownload.java @@ -0,0 +1,434 @@ +package com.fongmi.android.tv.utils; + +import android.text.TextUtils; + +import com.fongmi.android.tv.App; +import com.fongmi.android.tv.api.config.LiveConfig; +import com.fongmi.android.tv.api.config.VodConfig; +import com.fongmi.android.tv.bean.Live; +import com.fongmi.android.tv.bean.Site; +import com.github.catvod.net.OkHttp; +import com.github.catvod.utils.Path; +import com.github.catvod.utils.Util; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import okhttp3.Response; + +public class ConfigDownload { + + public interface Listener { + void onProgress(String stage, int current, int total); + void onSuccess(String dirName); + void onError(String msg); + } + + public static boolean shouldShow(String url) { + boolean empty = TextUtils.isEmpty(url); + boolean startsHttp = !empty && url.toLowerCase(Locale.ROOT).startsWith("http"); + boolean local = startsHttp && (url.startsWith("http://127.") || url.startsWith("https://127.")); + return !empty && startsHttp && !local; + } + + private static String cleanJar(String jar) { + if (TextUtils.isEmpty(jar)) return jar; + int idx = jar.indexOf(";md5;"); + return idx >= 0 ? jar.substring(0, idx) : jar; + } + + private static String jarFingerprint(String rawJar) { + if (TextUtils.isEmpty(rawJar)) return ""; + int idx = rawJar.indexOf(";md5;"); + if (idx >= 0) { + String md5 = rawJar.substring(idx + 5).trim(); + if (md5.startsWith("http")) { + try { md5 = OkHttp.string(md5).trim(); } catch (Exception ignored) {} + } + if (!TextUtils.isEmpty(md5)) return md5.toLowerCase(Locale.ROOT); + } + return Util.md5(cleanJar(rawJar)); + } + + private static String computeGlobalSpider() { + Map counts = new HashMap<>(); + for (Site site : VodConfig.get().getSites()) { + String jar = cleanJar(site.getJar()); + if (!TextUtils.isEmpty(jar)) counts.merge(jar, 1, Integer::sum); + } + if (counts.isEmpty()) return null; + String best = null; + int max = 0; + for (Map.Entry e : counts.entrySet()) { + if (e.getValue() > max) { max = e.getValue(); best = e.getKey(); } + } + return best; + } + + public static void start(String url, Listener listener) { + new Thread(() -> { + try { + String dirName = extractDirName(url); + String outDir = "tvbox/api/" + dirName; + String clanPrefix = "clan://api/" + dirName; + File outRoot = new File(Path.root(), outDir); + outRoot.mkdirs(); + + JsonObject root = new JsonObject(); + Set savedNames = new HashSet<>(); + Map jarCache = new HashMap<>(); + + String globalSpider = computeGlobalSpider(); + + int total = countTotal(); + final int totalRef = total; + final int[] progress = new int[]{0}; + + notify(listener, "准备下载", progress[0], totalRef); + + if (!TextUtils.isEmpty(globalSpider)) { + String jarRel = ensureJar(globalSpider, outDir, clanPrefix, jarCache, () -> { + progress[0]++; + notify(listener, "下载 jar", progress[0], totalRef); + }); + if (jarRel != null) root.addProperty("spider", jarRel); + } + + JsonArray sitesArr = new JsonArray(); + for (Site site : VodConfig.get().getSites()) { + JsonObject so = new JsonObject(); + so.addProperty("key", safeStr(site.getKey())); + so.addProperty("name", safeStr(site.getName())); + so.addProperty("type", site.getType()); + + String api = safeStr(site.getApi()); + boolean isJsPy = isJsOrPyFile(api); + if (isJsPy) { + String ext = api.endsWith(".js") ? "js" : "py"; + String fname = uniqueName(stripSlash(getBasename(api)), savedNames); + downloadText(api, outDir + "/" + ext + "/" + fname); + api = clanPrefix + "/" + ext + "/" + fname; + progress[0]++; + notify(listener, "下载 " + ext, progress[0], totalRef); + } + so.addProperty("api", api); + + String jar = cleanJar(site.getJar()); + if (!isJsPy && !TextUtils.isEmpty(jar) && !jar.equals(globalSpider)) { + String jarRel = ensureJar(site.getJar(), outDir, clanPrefix, jarCache, () -> { + progress[0]++; + notify(listener, "下载 jar", progress[0], totalRef); + }); + if (jarRel != null) so.addProperty("jar", jarRel); + } + + String extRaw = site.getExt(); + if (!TextUtils.isEmpty(extRaw)) { + String trimmed = extRaw.trim(); + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + try { + JsonElement el = App.gson().fromJson(trimmed, JsonElement.class); + so.add("ext", el); + } catch (Exception ignored) { + so.addProperty("ext", extRaw); + } + } else if (isJsonPath(trimmed)) { + String fname = uniqueName(stripSlash(getBasename(trimmed)), savedNames); + downloadText(trimmed, outDir + "/json/" + fname); + so.addProperty("ext", clanPrefix + "/json/" + fname); + progress[0]++; + notify(listener, "下载 json", progress[0], totalRef); + } else { + so.addProperty("ext", extRaw); + } + } + sitesArr.add(so); + } + root.add("sites", sitesArr); + + JsonArray livesArr = new JsonArray(); + for (Live live : LiveConfig.get().getLives()) { + String liveUrl = safeStr(live.getUrl()); + if (TextUtils.isEmpty(liveUrl)) continue; + if (liveUrl.startsWith("clan://lives/")) { + String fname = uniqueName(stripSlash(getBasename(liveUrl)), savedNames); + downloadText(liveUrl, outDir + "/lives/" + fname); + liveUrl = clanPrefix + "/lives/" + fname; + progress[0]++; + notify(listener, "下载直播源", progress[0], totalRef); + } + JsonObject lo = new JsonObject(); + lo.addProperty("name", safeStr(live.getName())); + lo.addProperty("url", liveUrl); + livesArr.add(lo); + } + root.add("lives", livesArr); + + notify(listener, "生成配置文件", progress[0], totalRef); + String pretty = prettyPrint(root); + File outFile = new File(outRoot, dirName + ".json"); + Path.write(outFile, pretty.getBytes("UTF-8")); + + App.post(() -> { + if (listener != null) listener.onProgress("完成", totalRef, totalRef); + }); + App.post(() -> { + if (listener != null) listener.onSuccess(dirName); + }); + } catch (Throwable e) { + App.post(() -> { + if (listener != null) listener.onError(e.getMessage() == null ? "unknown" : e.getMessage()); + }); + } + }).start(); + } + + private static int countTotal() { + int count = 0; + String globalSpider = computeGlobalSpider(); + if (!TextUtils.isEmpty(globalSpider)) count++; + Map jarCache = new HashMap<>(); + for (Site site : VodConfig.get().getSites()) { + String api = safeStr(site.getApi()); + if (isJsOrPyFile(api)) count++; + String jar = cleanJar(site.getJar()); + if (!TextUtils.isEmpty(jar) && !jar.equals(globalSpider)) { + String fp = jarFingerprint(site.getJar()); + if (!TextUtils.isEmpty(fp) && !jarCache.containsKey(fp)) { + jarCache.put(fp, jar); + count++; + } + } + String extRaw = site.getExt(); + if (!TextUtils.isEmpty(extRaw) && isJsonPath(extRaw.trim())) count++; + } + for (Live live : LiveConfig.get().getLives()) { + String liveUrl = safeStr(live.getUrl()); + if (!TextUtils.isEmpty(liveUrl) && liveUrl.startsWith("clan://lives/")) count++; + } + count++; + return count; + } + + private static void notify(Listener listener, String stage, int current, int total) { + App.post(() -> { + if (listener != null) listener.onProgress(stage, current, total); + }); + } + + private static String ensureJar(String rawJar, String outDir, String clanPrefix, Map jarCache, Runnable downloaded) { + String fp = jarFingerprint(rawJar); + if (TextUtils.isEmpty(fp)) return null; + String cached = jarCache.get(fp); + if (cached != null) return cached; + String fname = stripSlash(getBasename(cleanJar(rawJar))); + if (TextUtils.isEmpty(fname)) fname = fp + ".jar"; + String relDir = outDir + "/jars/"; + try { + downloadJar(cleanJar(rawJar), relDir + fname); + if (downloaded != null) downloaded.run(); + } catch (Exception ignored) { + } + String rel = clanPrefix + "/jars/" + fname; + jarCache.put(fp, rel); + return rel; + } + + private static String extractDirName(String url) { + String host = url; + try { + int q = host.indexOf('?'); + if (q >= 0) host = host.substring(0, q); + int s = host.indexOf("://"); + if (s >= 0) host = host.substring(s + 3); + int slash = host.indexOf('/'); + if (slash > 0) host = host.substring(0, slash); + int port = host.indexOf(':'); + if (port > 0) host = host.substring(0, port); + } catch (Exception ignored) {} + host = host.replaceAll("[^a-zA-Z0-9_-]", "_"); + if (host.isEmpty()) host = "config"; + if (host.length() > 20) host = host.substring(0, 20); + String stamp = new SimpleDateFormat("HHmmss", Locale.ROOT).format(new Date()); + return host + "_" + stamp; + } + + private static void downloadJar(String src, String relOut) throws Exception { + File out = new File(Path.root(), relOut); + File parent = out.getParentFile(); + if (parent != null) parent.mkdirs(); + if (out.exists() && out.length() > 0) return; + byte[] data = fetchBytes(src); + if (data != null && data.length > 0) { + Path.write(out, data); + } else { + File local = Path.local(src); + if (local != null && local.exists()) { + Path.write(out, Path.readToByte(local)); + } + } + } + + private static void downloadText(String src, String relOut) { + try { + File out = new File(Path.root(), relOut); + File parent = out.getParentFile(); + if (parent != null) parent.mkdirs(); + String content = null; + if (src.startsWith("http")) { + content = OkHttp.string(src); + } else { + File local = Path.local(src); + if (local != null && local.exists()) { + content = Path.read(local); + } + } + if (!TextUtils.isEmpty(content)) { + Path.write(out, content.getBytes("UTF-8")); + } + } catch (Exception ignored) { + } + } + + private static byte[] fetchBytes(String url) throws IOException { + if (url.startsWith("http")) { + Response res = null; + try { + res = OkHttp.newCall(url).execute(); + if (res.body() != null) { + return res.body().bytes(); + } + } finally { + if (res != null) res.close(); + } + } + return null; + } + + private static boolean isJsOrPyFile(String api) { + return api != null && (api.endsWith(".js") || api.endsWith(".py")); + } + + private static boolean isJsonPath(String ext) { + if (ext == null) return false; + String t = ext.trim(); + if (t.endsWith(".json")) return true; + if (t.startsWith("http") && t.contains(".json")) return true; + return false; + } + + private static String getBasename(String path) { + if (path == null) return "resource"; + int slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + return slash >= 0 ? path.substring(slash + 1) : path; + } + + private static String stripSlash(String s) { + while (s.startsWith("/")) s = s.substring(1); + return s; + } + + private static String uniqueName(String name, Set used) { + if (!used.contains(name)) { + used.add(name); + return name; + } + String base = name; + int dot = name.lastIndexOf('.'); + String ext = ""; + if (dot > 0) { + base = name.substring(0, dot); + ext = name.substring(dot); + } + for (int i = 2; ; i++) { + String candidate = base + "_" + i + ext; + if (!used.contains(candidate)) { + used.add(candidate); + return candidate; + } + } + } + + private static String safeStr(String s) { + return s == null ? "" : s; + } + + private static String prettyPrint(JsonElement el) { + StringBuilder sb = new StringBuilder(); + appendPretty(sb, el, 0); + sb.append('\n'); + return sb.toString(); + } + + private static void appendPretty(StringBuilder sb, JsonElement el, int indent) { + if (el == null || el.isJsonNull()) { + sb.append("null"); + } else if (el.isJsonPrimitive()) { + sb.append(el.getAsJsonPrimitive().toString()); + } else if (el.isJsonArray()) { + JsonArray arr = el.getAsJsonArray(); + if (arr.size() == 0) { + sb.append("[]"); + } else { + sb.append("[\n"); + for (int i = 0; i < arr.size(); i++) { + indent(sb, indent + 1); + appendPretty(sb, arr.get(i), indent + 1); + if (i < arr.size() - 1) sb.append(','); + sb.append('\n'); + } + indent(sb, indent); + sb.append(']'); + } + } else if (el.isJsonObject()) { + JsonObject obj = el.getAsJsonObject(); + if (obj.entrySet().isEmpty()) { + sb.append("{}"); + } else { + sb.append("{\n"); + int i = 0; + for (Map.Entry e : obj.entrySet()) { + indent(sb, indent + 1); + sb.append('"').append(escape(e.getKey())).append("\": "); + appendPretty(sb, e.getValue(), indent + 1); + if (i < obj.entrySet().size() - 1) sb.append(','); + sb.append('\n'); + i++; + } + indent(sb, indent); + sb.append('}'); + } + } + } + + private static void indent(StringBuilder sb, int n) { + for (int i = 0; i < n; i++) sb.append(" "); + } + + private static String escape(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': sb.append("\\\""); break; + case '\\': sb.append("\\\\"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: sb.append(c); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java b/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java index e7ef2784e0d..25c41a679c5 100644 --- a/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java +++ b/app/src/main/java/com/fongmi/android/tv/utils/FileUtil.java @@ -142,7 +142,9 @@ public static StorageSpace unavailable() { } public static Uri getShareUri(String path) { - return getShareUri(new File(path.replace("file://", ""))); + if (path == null) return null; + File file = com.github.catvod.utils.Path.local(path); + return file != null ? getShareUri(file) : null; } public static Uri getShareUri(File file) { diff --git a/app/src/main/java/com/fongmi/android/tv/utils/LoginStateSync.java b/app/src/main/java/com/fongmi/android/tv/utils/LoginStateSync.java index e52d0846f19..60172ceb324 100644 --- a/app/src/main/java/com/fongmi/android/tv/utils/LoginStateSync.java +++ b/app/src/main/java/com/fongmi/android/tv/utils/LoginStateSync.java @@ -1086,10 +1086,12 @@ private static String parent(String path) { private static String normalize(String path) { if (path == null) return ""; + File localFile = com.github.catvod.utils.Path.local(path); + String absolute = localFile != null ? localFile.getAbsolutePath() : path; String appRoot = appRoot().getAbsolutePath().replace('\\', '/'); String dataRoot = new File(App.get().getApplicationInfo().dataDir).getAbsolutePath().replace('\\', '/'); String sdcardRoot = sdcardRoot().getAbsolutePath().replace('\\', '/'); - String value = path.trim().replace('\\', '/').replace("file://", ""); + String value = absolute.trim().replace('\\', '/'); if (value.startsWith(appRoot)) value = ROOT_APP + "/" + value.substring(appRoot.length()); else if (value.startsWith(dataRoot)) value = ROOT_APP + "/" + value.substring(dataRoot.length()); else if (value.startsWith(sdcardRoot)) value = ROOT_SDCARD + "/" + value.substring(sdcardRoot.length()); diff --git a/app/src/main/java/com/fongmi/android/tv/utils/SearchModeStore.java b/app/src/main/java/com/fongmi/android/tv/utils/SearchModeStore.java new file mode 100644 index 00000000000..c498a779544 --- /dev/null +++ b/app/src/main/java/com/fongmi/android/tv/utils/SearchModeStore.java @@ -0,0 +1,114 @@ +package com.fongmi.android.tv.utils; + +import android.text.TextUtils; + +import com.fongmi.android.tv.App; +import com.fongmi.android.tv.api.config.VodConfig; +import com.fongmi.android.tv.bean.Site; +import com.github.catvod.utils.Prefers; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class SearchModeStore { + + public static final int MODE_ALL = 0; + public static final int MODE_HOME = 1; + public static final int MODE_TAG = 2; + public static final int MODE_WHITE = 3; + public static final int MODE_BLACK = 4; + + private static final String KEY_MODE = "search_mode"; + private static final String KEY_WHITE = "search_white_list"; + private static final String KEY_BLACK = "search_black_list"; + private static final String KEY_TAG = "search_tag_groups"; + + private static final Type STRING_LIST = new TypeToken>() {}.getType(); + + public static int getMode() { + return Prefers.getInt(KEY_MODE, MODE_ALL); + } + + public static void putMode(int mode) { + Prefers.put(KEY_MODE, mode); + } + + public static List getWhiteList() { + return safeList(Prefers.getString(KEY_WHITE, "[]")); + } + + public static void putWhiteList(List keys) { + Prefers.put(KEY_WHITE, App.gson().toJson(keys == null ? new ArrayList<>() : keys)); + } + + public static List getBlackList() { + return safeList(Prefers.getString(KEY_BLACK, "[]")); + } + + public static void putBlackList(List keys) { + Prefers.put(KEY_BLACK, App.gson().toJson(keys == null ? new ArrayList<>() : keys)); + } + + public static List getTagGroups() { + return safeList(Prefers.getString(KEY_TAG, "[]")); + } + + public static void putTagGroups(List groups) { + Prefers.put(KEY_TAG, App.gson().toJson(groups == null ? new ArrayList<>() : groups)); + } + + private static List safeList(String json) { + try { + if (TextUtils.isEmpty(json)) return new ArrayList<>(); + List list = App.gson().fromJson(json, STRING_LIST); + return list == null ? new ArrayList<>() : list; + } catch (Exception e) { + return new ArrayList<>(); + } + } + + public static List filterSites(List all, String forceSiteKey) { + int mode = getMode(); + List result = new ArrayList<>(); + if (mode == MODE_HOME) { + Site home = VodConfig.get().getHome(); + if (home != null && all.contains(home)) result.add(home); + else if (home != null) result.add(home); + return result; + } + if (!TextUtils.isEmpty(forceSiteKey)) { + for (Site s : all) { + if (s.getKey().equals(forceSiteKey)) { result.add(s); return result; } + } + return result; + } + switch (mode) { + case MODE_ALL: + for (Site s : all) if (s.isSearchable()) result.add(s); + break; + case MODE_TAG: + List groups = getTagGroups(); + if (groups.isEmpty()) { + for (Site s : all) if (s.isSearchable()) result.add(s); + } else { + for (Site s : all) { + for (String g : groups) { + if (s.inGroup(g)) { result.add(s); break; } + } + } + } + break; + case MODE_WHITE: + List white = getWhiteList(); + for (Site s : all) if (white.contains(s.getKey())) result.add(s); + break; + case MODE_BLACK: + List black = getBlackList(); + for (Site s : all) if (!black.contains(s.getKey()) && s.isSearchable()) result.add(s); + break; + } + return result; + } +} diff --git a/app/src/main/java/com/fongmi/android/tv/utils/SearchSourceHelper.java b/app/src/main/java/com/fongmi/android/tv/utils/SearchSourceHelper.java new file mode 100644 index 00000000000..17caf4235fd --- /dev/null +++ b/app/src/main/java/com/fongmi/android/tv/utils/SearchSourceHelper.java @@ -0,0 +1,61 @@ +package com.fongmi.android.tv.utils; + +import com.fongmi.android.tv.api.config.VodConfig; +import com.fongmi.android.tv.bean.Site; +import com.fongmi.android.tv.R; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SearchSourceHelper { + + public static List buildItems() { + List items = new ArrayList<>(); + items.add(new SearchSourceItem(SearchModeStore.MODE_ALL, R.string.search_source_all, false)); + SearchSourceItem home = new SearchSourceItem(SearchModeStore.MODE_HOME, R.string.search_source_home, false); + Site site = VodConfig.get().getHome(); + home.subtitle = site != null ? site.getName() : ""; + items.add(home); + SearchSourceItem tag = new SearchSourceItem(SearchModeStore.MODE_TAG, R.string.search_source_tag, true); + tag.subtitle = buildTagSubtitle(); + items.add(tag); + SearchSourceItem white = new SearchSourceItem(SearchModeStore.MODE_WHITE, R.string.search_source_white, true); + int wn = SearchModeStore.getWhiteList().size(); + white.subtitle = wn > 0 ? String.valueOf(wn) : ""; + items.add(white); + SearchSourceItem black = new SearchSourceItem(SearchModeStore.MODE_BLACK, R.string.search_source_black, true); + int bn = SearchModeStore.getBlackList().size(); + black.subtitle = bn > 0 ? String.valueOf(bn) : ""; + items.add(black); + return items; + } + + private static String buildTagSubtitle() { + List groups = SearchModeStore.getTagGroups(); + if (groups.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < groups.size(); i++) { + if (i > 0) sb.append(" "); + sb.append(groups.get(i)); + } + return sb.toString(); + } + + public static List buildPickerSites(boolean tagMode) { + if (tagMode) { + List pseudo = new ArrayList<>(); + Set groups = new HashSet<>(); + for (Site s : VodConfig.get().getSites()) groups.addAll(s.getGroups()); + for (String g : groups) { + Site ps = new Site(); + ps.setKey(g); + ps.setName(g); + pseudo.add(ps); + } + return pseudo; + } + return VodConfig.get().getSites(); + } +} diff --git a/app/src/main/java/com/fongmi/android/tv/utils/SearchSourceItem.java b/app/src/main/java/com/fongmi/android/tv/utils/SearchSourceItem.java new file mode 100644 index 00000000000..57313c55800 --- /dev/null +++ b/app/src/main/java/com/fongmi/android/tv/utils/SearchSourceItem.java @@ -0,0 +1,15 @@ +package com.fongmi.android.tv.utils; + +public class SearchSourceItem { + + public final int mode; + public final int titleRes; + public final boolean configurable; + public String subtitle; + + public SearchSourceItem(int mode, int titleRes, boolean configurable) { + this.mode = mode; + this.titleRes = titleRes; + this.configurable = configurable; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/utils/SyncFiles.java b/app/src/main/java/com/fongmi/android/tv/utils/SyncFiles.java index 9fe06c683cf..fff8b65a7e1 100644 --- a/app/src/main/java/com/fongmi/android/tv/utils/SyncFiles.java +++ b/app/src/main/java/com/fongmi/android/tv/utils/SyncFiles.java @@ -23,7 +23,7 @@ public class SyncFiles { - public static final String DEFAULT_PATHS = "TV\nTVBox\nTVData"; + public static final String DEFAULT_PATHS = ""; public static final String CUSTOM_CSP_PATH = "TV/CustomCsp"; public static final String PART_NAME = "syncFiles"; @@ -215,9 +215,10 @@ private static boolean inside(File root, File file) throws IOException { public static String normalize(String path) { if (path == null) return ""; + File localFile = com.github.catvod.utils.Path.local(path); + String absolute = localFile != null ? localFile.getAbsolutePath() : path; String root = Path.root().getAbsolutePath().replace('\\', '/'); - String value = path.trim().replace('\\', '/'); - value = value.replace("file://", ""); + String value = absolute.trim().replace('\\', '/'); if (value.startsWith(root)) value = value.substring(root.length()); if (value.startsWith("/sdcard/")) value = value.substring("/sdcard/".length()); return normalizeRelative(value); diff --git a/app/src/main/java/com/fongmi/android/tv/utils/UrlUtil.java b/app/src/main/java/com/fongmi/android/tv/utils/UrlUtil.java index 3b436258f1c..8663bd76f43 100644 --- a/app/src/main/java/com/fongmi/android/tv/utils/UrlUtil.java +++ b/app/src/main/java/com/fongmi/android/tv/utils/UrlUtil.java @@ -6,6 +6,8 @@ import com.github.catvod.utils.UriUtil; import com.google.common.net.HttpHeaders; +import java.io.File; + public class UrlUtil { public static Uri uri(String url) { @@ -44,6 +46,8 @@ public static String resolve(String baseUri, String referenceUri) { } public static String convert(String url) { + if (url == null) return null; + url = url.replace("clan://", "file://tvbox/"); String scheme = scheme(url); String path = null; if ("assets".equals(scheme)) path = "/"; @@ -52,6 +56,12 @@ public static String convert(String url) { return path != null ? url.replace(scheme + "://", Server.get().getAddress(path)) : url; } + public static File toLocalFile(String url) { + if (url == null) return null; + if (url.startsWith("http") || url.startsWith("content")) return null; + return com.github.catvod.utils.Path.local(url); + } + public static String getName(String url) { Uri uri = Uri.parse(url); String path = path(uri); @@ -65,4 +75,4 @@ public static String fixHeader(String key) { if (HttpHeaders.COOKIE.equalsIgnoreCase(key)) return HttpHeaders.COOKIE; return key; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fongmi/android/tv/web/ext/WebHomeExtension.java b/app/src/main/java/com/fongmi/android/tv/web/ext/WebHomeExtension.java index a4d72e96579..7d9c5d6f0a9 100644 --- a/app/src/main/java/com/fongmi/android/tv/web/ext/WebHomeExtension.java +++ b/app/src/main/java/com/fongmi/android/tv/web/ext/WebHomeExtension.java @@ -234,6 +234,7 @@ public static boolean isScriptUrl(String url) { private static String readCached(String url, String prefix, String suffix) { if (TextUtils.isEmpty(url)) return ""; + url = url.replace("clan://", "file://tvbox/"); if ("file".equals(UrlUtil.scheme(url))) return Path.read(Path.local(url.substring("file://".length()))); if ("local".equals(UrlUtil.scheme(url))) return Path.read(Path.files(url.substring("local://".length()))); File file = cache(prefix + Util.md5(url) + suffix); diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b0af7788998..e499938c315 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,5 +1,5 @@ - - + + 影视 @@ -367,6 +367,16 @@ 已保存到对方设备 保存到对方设备失败:%1$s 登录态学习 + 来源屏蔽 + 选择要屏蔽的站源来源 + 已屏蔽 %1$d / %2$d + 点播地址 + 直播地址 + sites-json + sites-js + sites-py + sites + lives 文件 站点健康排序 启用站点健康排序 站点弹窗排序 @@ -672,6 +682,10 @@ 语言 图片尺寸 界面大小 + 标题换行 + 删除文件站点 + 确定删除 %1$s 及其源文件?此操作不可恢复。 + 已删除 %1$s 关闭 DoH Proxy @@ -735,8 +749,8 @@ 中转服务配置 同步目录 编辑 - 每行一个外置存储相对路径,默认同步 TV、TVBox、TVData。可加入本地包目录;目录内容会随 Jar/脚本保存数据一起同步。 - TV TVBox TVData + 每行一个外置存储相对路径,不默认同步任何目录,需自行勾选。目录内容会随 Jar/脚本保存数据一起同步。 + 收起 刷新 打开目录 @@ -1200,6 +1214,7 @@ 跟随系统 + 稍宽松 标准 较紧凑 紧凑 @@ -1207,6 +1222,13 @@ 更小 + + 关闭 + 最多2行 + 最多3行 + 不限行数 + + 跟随系统 简体中文 @@ -1439,4 +1461,18 @@ 队列 循环 + + 配置 + 确定 + 搜索站点… + 全选 + 全清 + 反选 + 全部源 + 首页源 + 标签源 + 白名单 + 黑名单 + %d 个站点 + 已选 %d / %d diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3d685a34aad..c9ca8feb5f4 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,5 +1,5 @@ - - + + 影視 @@ -367,6 +367,16 @@ 已保存到對方設備 保存到對方設備失敗:%1$s 登入狀態學習 + 來源屏蔽 + 選擇要屏蔽的站源來源 + 已屏蔽 %1$d / %2$d + 點播位址 + 直播位址 + sites-json + sites-js + sites-py + sites + lives 檔案 站點健康排序 啟用站點健康排序 站點彈窗排序 @@ -656,6 +666,10 @@ 語言 圖片尺寸 介面大小 + 標題換行 + 刪除文件站點 + 確定刪除 %1$s 及其源文件?此操作不可恢復。 + 已刪除 %1$s 關閉 DoH Proxy @@ -719,8 +733,8 @@ 中轉服務配置 同步目錄 編輯 - 每行一個外置儲存相對路徑,預設同步 TV、TVBox、TVData。可加入本地包目錄;目錄內容會隨 Jar/腳本保存資料一起同步。 - TV TVBox TVData + 每行一個外置儲存相對路徑,不預設同步任何目錄,需自行勾選。目錄內容會隨 Jar/腳本保存資料一起同步。 + 收起 刷新 打開目錄 @@ -1184,6 +1198,7 @@ 跟隨系統 + 稍寬鬆 標準 較緊湊 緊湊 @@ -1191,6 +1206,13 @@ 更小 + + 關閉 + 最多2行 + 最多3行 + 不限行數 + + 跟隨系統 簡體中文 @@ -1423,4 +1445,18 @@ 佇列 循環 + + 設定 + 確定 + 搜尋站點… + 全選 + 全清 + 反選 + 全部來源 + 首頁來源 + 標籤來源 + 白名單 + 黑名單 + %d 個站點 + 已選 %d / %d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a86fbb6b59..50a2803ca57 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - - + + TV @@ -369,6 +369,16 @@ Failed to save on the other device: %1$s Select Login state learning + Source blocking + Select sources to block + Blocked %1$d / %2$d + Vod URL + Live URL + sites-json + sites-js + sites-py + sites + lives files Site health ranking Enable site health ranking Sort site dialog @@ -674,6 +684,10 @@ Language Image size Interface size + Title wrapping + Delete file site + Delete %1$s and its source file? This cannot be undone. + Deleted %1$s Close DoH Proxy @@ -753,8 +767,8 @@ Sync data received Sync folders Edit - One external-storage relative path per line. TV, TVBox and TVData are synced by default. You can add local package folders; folder contents are synced with Jar/script saved data. - TV TVBox TVData + One external-storage relative path per line. Select folders in the directory tree below; folder contents are synced with Jar/script saved data. + Collapse Refresh Open folder @@ -1208,6 +1222,7 @@ Follow system + Slightly relaxed Standard Mild compact Compact @@ -1215,6 +1230,13 @@ Smaller + + Off + Up to 2 lines + Up to 3 lines + Unlimited + + Follow system Simplified Chinese @@ -1447,4 +1469,18 @@ Queue Repeat + + Config + OK + Search sites… + Select all + Select none + Invert + All sources + Home source + Tag source + Whitelist + Blacklist + %d site(s) + Selected %d / %d diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/activity/HomeActivity.java b/app/src/mobile/java/com/fongmi/android/tv/ui/activity/HomeActivity.java index 23293ca37a0..378e7d1dbaa 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/activity/HomeActivity.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/activity/HomeActivity.java @@ -96,7 +96,10 @@ protected void initView(Bundle savedInstanceState) { mChrome = new WebHomeChromeController(this, mBinding, this, savedInstanceState, WebHomeChromeStartup.restore(mStartupConfig)); mBinding.getRoot().addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> checkWindowShape(right - left, bottom - top)); mBinding.navigation.setOnItemSelectedListener(this); - PermissionUtil.requestFile(this, allGranted -> PermissionUtil.requestNotify(this)); + PermissionUtil.requestFile(this, allGranted -> { + PermissionUtil.requestNotify(this); + if (allGranted) initConfig(); + }); initFragment(savedInstanceState); initConfig(); } @@ -155,8 +158,8 @@ private void restorePosition(int position) { changeFragment(position <= 0 ? 0 : position); } - private void initConfig() { - VodConfig.get().config(mStartupConfig == null ? Config.vod() : mStartupConfig).load(getCallback()); + public void initConfig() { + VodConfig.get().init().load(getCallback()); LiveConfig.get().init().load(); WallConfig.get().init(); } @@ -190,7 +193,7 @@ public void success() { private void setNavigation() { mBinding.navigation.getMenu().findItem(R.id.vod).setVisible(true); mBinding.navigation.getMenu().findItem(R.id.setting).setVisible(true); - mBinding.navigation.getMenu().findItem(R.id.live).setVisible(LiveConfig.hasUrl()); + mBinding.navigation.getMenu().findItem(R.id.live).setVisible(LiveConfig.hasLoadedLives()); syncNavigationSelection(); } @@ -439,4 +442,4 @@ protected void onDestroy() { Server.get().stop(); super.onDestroy(); } -} +} \ No newline at end of file diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java index d760af091b2..ad8d855253f 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SearchAdapter.java @@ -11,6 +11,7 @@ import com.fongmi.android.tv.bean.Vod; import com.fongmi.android.tv.databinding.AdapterSearchBinding; import com.fongmi.android.tv.databinding.AdapterVodRectBinding; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.utils.ImgUtil; public class SearchAdapter extends BaseDiffAdapter { @@ -79,11 +80,12 @@ public class ListHolder extends RecyclerView.ViewHolder { super(binding.getRoot()); this.binding = binding; binding.getRoot().setFocusable(true); - binding.name.setMarqueeRepeatLimit(-1); binding.getRoot().setOnFocusChangeListener((view, hasFocus) -> setMarquee(hasFocus)); } private void initView(Vod item) { + Setting.applyTitleMaxLines(binding.name); + binding.name.setHorizontallyScrolling(Setting.resolveTitleMaxLines() <= 1); binding.name.setText(item.getName()); setMarquee(binding.getRoot().hasFocus()); binding.site.setText(item.getSiteName()); @@ -95,11 +97,10 @@ private void initView(Vod item) { } private void setMarquee(boolean focused) { - binding.name.setSingleLine(focused); - if (!focused) binding.name.setMaxLines(3); - binding.name.setHorizontallyScrolling(focused); - binding.name.setEllipsize(focused ? TextUtils.TruncateAt.MARQUEE : TextUtils.TruncateAt.END); - binding.name.setSelected(focused); + if (Setting.resolveTitleMaxLines() <= 1) { + binding.name.setEllipsize(focused ? TextUtils.TruncateAt.MARQUEE : TextUtils.TruncateAt.END); + binding.name.setSelected(focused); + } } } @@ -111,15 +112,14 @@ public class GridHolder extends RecyclerView.ViewHolder { super(binding.getRoot()); this.binding = binding; binding.getRoot().setFocusable(true); - binding.name.setSingleLine(true); - binding.name.setHorizontallyScrolling(true); - binding.name.setMarqueeRepeatLimit(-1); binding.getRoot().setOnFocusChangeListener((view, hasFocus) -> setMarquee(hasFocus)); applySize(); } private void initView(Vod item) { applySize(); + Setting.applyTitleMaxLines(binding.name); + binding.name.setHorizontallyScrolling(Setting.resolveTitleMaxLines() <= 1); binding.name.setText(item.getName()); setMarquee(binding.getRoot().hasFocus()); binding.site.setText(item.getSiteName()); @@ -146,8 +146,10 @@ private void applySize() { } private void setMarquee(boolean focused) { - binding.name.setEllipsize(focused ? TextUtils.TruncateAt.MARQUEE : TextUtils.TruncateAt.END); - binding.name.setSelected(focused); + if (Setting.resolveTitleMaxLines() <= 1) { + binding.name.setEllipsize(focused ? TextUtils.TruncateAt.MARQUEE : TextUtils.TruncateAt.END); + binding.name.setSelected(focused); + } } } } diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SearchSourceAdapter.java b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SearchSourceAdapter.java new file mode 100644 index 00000000000..d6aea2771d0 --- /dev/null +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SearchSourceAdapter.java @@ -0,0 +1,96 @@ +package com.fongmi.android.tv.ui.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fongmi.android.tv.databinding.ItemSearchSourceBinding; +import com.fongmi.android.tv.utils.SearchSourceItem; + +import java.util.ArrayList; +import java.util.List; + +public class SearchSourceAdapter extends RecyclerView.Adapter { + + private final List items = new ArrayList<>(); + private int checked = 0; + private OnConfigListener configListener; + private OnModeChangeListener modeChangeListener; + + public interface OnConfigListener { + void onConfig(SearchSourceItem item); + } + + public interface OnModeChangeListener { + void onModeChanged(int mode); + } + + public void setOnConfigListener(OnConfigListener l) { + this.configListener = l; + } + + public void setOnModeChangeListener(OnModeChangeListener l) { + this.modeChangeListener = l; + } + + public void setItems(List items, int checked) { + this.items.clear(); + this.items.addAll(items); + this.checked = checked; + notifyDataSetChanged(); + } + + public int getChecked() { + return checked; + } + + private int findPositionOfMode(int mode) { + for (int i = 0; i < items.size(); i++) if (items.get(i).mode == mode) return i; + return -1; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemSearchSourceBinding binding = ItemSearchSourceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + SearchSourceItem item = items.get(position); + holder.binding.title.setText(item.titleRes); + holder.binding.subtitle.setText(item.subtitle == null ? "" : item.subtitle); + holder.binding.subtitle.setVisibility(item.subtitle == null || item.subtitle.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE); + holder.binding.radio.setChecked(checked == item.mode); + holder.binding.config.setVisibility(item.configurable ? android.view.View.VISIBLE : android.view.View.GONE); + holder.binding.getRoot().setOnClickListener(v -> { + int prev = checked; + checked = item.mode; + int p1 = findPositionOfMode(prev); + int p2 = findPositionOfMode(checked); + if (p1 >= 0) notifyItemChanged(p1); + if (p2 >= 0) notifyItemChanged(p2); + if (modeChangeListener != null) modeChangeListener.onModeChanged(checked); + }); + holder.binding.config.setOnClickListener(v -> { + if (configListener != null) configListener.onConfig(item); + }); + } + + @Override + public int getItemCount() { + return items.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ItemSearchSourceBinding binding; + + ViewHolder(ItemSearchSourceBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java index fdbb8700e8f..6528551a68e 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SiteAdapter.java @@ -49,6 +49,8 @@ public interface OnClickListener { void onChangeClick(int position, Site item); + void onDeleteClick(int position, Site item); + boolean onTextLongClick(ViewHolder holder); boolean onSearchLongClick(Site item); @@ -174,9 +176,11 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.binding.change.setImageResource(getChangeIcon(item)); holder.binding.search.setVisibility(!block && search && singleColumn ? View.VISIBLE : View.GONE); holder.binding.change.setVisibility(!block && change && singleColumn ? View.VISIBLE : View.GONE); + holder.binding.delete.setVisibility(!block && item.isFile() && singleColumn ? View.VISIBLE : View.GONE); holder.binding.text.setOnClickListener(v -> listener.onTextClick(item)); holder.binding.search.setOnClickListener(v -> listener.onSearchClick(position, item)); holder.binding.change.setOnClickListener(v -> listener.onChangeClick(position, item)); + holder.binding.delete.setOnClickListener(v -> listener.onDeleteClick(position, item)); holder.binding.text.setOnLongClickListener(v -> listener.onTextLongClick(holder)); holder.binding.search.setOnLongClickListener(v -> listener.onSearchLongClick(item)); holder.binding.change.setOnLongClickListener(v -> listener.onChangeLongClick(item)); diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SiteCheckAdapter.java b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SiteCheckAdapter.java new file mode 100644 index 00000000000..9f917836e40 --- /dev/null +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/SiteCheckAdapter.java @@ -0,0 +1,111 @@ +package com.fongmi.android.tv.ui.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fongmi.android.tv.bean.Site; +import com.fongmi.android.tv.databinding.ItemSiteCheckBinding; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SiteCheckAdapter extends RecyclerView.Adapter { + + private List all = new ArrayList<>(); + private List filtered = new ArrayList<>(); + private final Set checkedKeys = new HashSet<>(); + + public void setItems(List sites, Set initial) { + all = new ArrayList<>(sites); + filtered = new ArrayList<>(sites); + checkedKeys.clear(); + if (initial != null) checkedKeys.addAll(initial); + notifyDataSetChanged(); + } + + public void filter(String keyword) { + if (keyword == null || keyword.isEmpty()) { + filtered = new ArrayList<>(all); + } else { + filtered = new ArrayList<>(); + String k = keyword.toLowerCase(); + for (Site s : all) { + if (s.getName().toLowerCase().contains(k) || s.getKey().toLowerCase().contains(k)) filtered.add(s); + } + } + notifyDataSetChanged(); + } + + public void selectAll() { + for (Site s : all) checkedKeys.add(s.getKey()); + notifyDataSetChanged(); + } + + public void selectNone() { + checkedKeys.clear(); + notifyDataSetChanged(); + } + + public void selectInvert() { + Set allKeys = new HashSet<>(); + for (Site s : all) allKeys.add(s.getKey()); + Set newChecked = new HashSet<>(allKeys); + newChecked.removeAll(checkedKeys); + checkedKeys.clear(); + checkedKeys.addAll(newChecked); + notifyDataSetChanged(); + } + + public Set getCheckedKeys() { + return new HashSet<>(checkedKeys); + } + + public int getCheckedCount() { + return checkedKeys.size(); + } + + public int getTotalCount() { + return all.size(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemSiteCheckBinding binding = ItemSiteCheckBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Site site = filtered.get(position); + holder.binding.check.setChecked(checkedKeys.contains(site.getKey())); + holder.binding.name.setText(site.getName()); + String typeText = site.getType() == 1 ? "JS" : site.getType() == 2 ? "PY" : "PQ"; + holder.binding.type.setText(typeText); + holder.binding.getRoot().setOnClickListener(v -> { + String key = site.getKey(); + if (checkedKeys.contains(key)) checkedKeys.remove(key); + else checkedKeys.add(key); + notifyItemChanged(position); + }); + } + + @Override + public int getItemCount() { + return filtered.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ItemSiteCheckBinding binding; + + ViewHolder(ItemSiteCheckBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/AppearanceDialog.java b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/AppearanceDialog.java index fb7c1d1e65e..d7cdf33a7f8 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/AppearanceDialog.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/AppearanceDialog.java @@ -26,10 +26,12 @@ public final class AppearanceDialog extends DialogFragment implements ThemeDialo private String[] uiScales; private String[] languages; private String[] imageSizes; + private String[] titleLines; private MaterialTextView uiScaleValue; private MaterialTextView themeValue; private MaterialTextView imageSizeValue; private MaterialTextView languageValue; + private MaterialTextView titleLinesValue; public static void show(Fragment fragment) { new AppearanceDialog().show(fragment.getChildFragmentManager(), AppearanceDialog.class.getSimpleName()); @@ -41,6 +43,7 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { uiScales = ResUtil.getStringArray(R.array.select_ui_scale); languages = ResUtil.getStringArray(R.array.select_language); imageSizes = ResUtil.getStringArray(R.array.select_size); + titleLines = ResUtil.getStringArray(R.array.select_title_lines); return LightDialog.create(requireContext(), getString(R.string.setting_appearance), createContent(), getString(R.string.dialog_close), null, null, null); } @@ -48,8 +51,9 @@ private View createContent() { LinearLayout content = new LinearLayout(requireContext()); content.setOrientation(LinearLayout.VERTICAL); uiScaleValue = addRow(content, R.string.setting_ui_scale, uiScales[Setting.getUiScaleIndex()], this::chooseUiScale); - themeValue = addRow(content, R.string.setting_theme_color, getThemeText(), view -> ThemeDialog.show(this)); imageSizeValue = addRow(content, R.string.setting_size, imageSizes[PlayerSetting.getSize()], this::chooseImageSize); + titleLinesValue = addRow(content, R.string.setting_title_lines, titleLines[Setting.getTitleLinesIndex()], this::chooseTitleLines); + themeValue = addRow(content, R.string.setting_theme_color, getThemeText(), view -> ThemeDialog.show(this)); languageValue = addRow(content, R.string.setting_language, languages[Setting.getLanguageIndex()], this::chooseLanguage); return content; } @@ -116,6 +120,15 @@ private void chooseLanguage(View view) { }); } + private void chooseTitleLines(View view) { + ChoiceDialog.showSingle(this, R.string.setting_title_lines, titleLines, Setting.getTitleLinesIndex(), which -> { + if (which == Setting.getTitleLinesIndex()) return; + titleLinesValue.setText(titleLines[which]); + Setting.putTitleLinesIndex(which); + RefreshEvent.size(); + }); + } + @Override public void setTheme(int color) { themeValue.setText(themeText(color)); @@ -136,4 +149,4 @@ private String themeText(int color) { private int dp(int value) { return ResUtil.dp2px(value); } -} +} \ No newline at end of file diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SearchSitePickerDialog.java b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SearchSitePickerDialog.java new file mode 100644 index 00000000000..8bf5a4c44f1 --- /dev/null +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SearchSitePickerDialog.java @@ -0,0 +1,98 @@ +package com.fongmi.android.tv.ui.dialog; + +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewbinding.ViewBinding; + +import com.fongmi.android.tv.R; +import com.fongmi.android.tv.databinding.DialogSitePickerBinding; +import com.fongmi.android.tv.ui.adapter.SiteCheckAdapter; +import com.fongmi.android.tv.utils.SearchSourceHelper; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class SearchSitePickerDialog extends BaseBottomSheetDialog { + + private DialogSitePickerBinding binding; + private SiteCheckAdapter adapter; + private final List initial; + private final boolean tagMode; + private Callback callback; + + public interface Callback { + void onConfirm(List keys); + } + + public static SearchSitePickerDialog createSite(String title, List initial, Callback callback) { + SearchSitePickerDialog d = new SearchSitePickerDialog(initial, false); + d.callback = callback; + return d; + } + + public static SearchSitePickerDialog createTag(List initial, Callback callback) { + SearchSitePickerDialog d = new SearchSitePickerDialog(initial, true); + d.callback = callback; + return d; + } + + private SearchSitePickerDialog(List initial, boolean tagMode) { + this.initial = initial == null ? new ArrayList<>() : new ArrayList<>(initial); + this.tagMode = tagMode; + } + + public void show(Fragment parent) { + show(parent.getChildFragmentManager(), null); + } + + @Override + protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) { + return binding = DialogSitePickerBinding.inflate(inflater, container, false); + } + + @Override + protected void initView() { + setupVisibility(); + adapter = new SiteCheckAdapter(); + binding.recycler.setAdapter(adapter); + adapter.setItems(SearchSourceHelper.buildPickerSites(tagMode), new HashSet<>(initial)); + updateCount(); + } + + private void setupVisibility() { + boolean isSite = !tagMode; + binding.search.setVisibility(isSite ? android.view.View.VISIBLE : android.view.View.GONE); + binding.count.setVisibility(isSite ? android.view.View.VISIBLE : android.view.View.GONE); + } + + private void updateCount() { + if (binding.count.getVisibility() == android.view.View.VISIBLE) { + binding.count.setText(getString(R.string.search_site_count_of, adapter.getCheckedCount(), adapter.getTotalCount())); + } + } + + @Override + protected void initEvent() { + binding.search.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override public void afterTextChanged(Editable s) { + if (adapter != null) adapter.filter(s.toString()); + } + }); + binding.selectAll.setOnClickListener(v -> { adapter.selectAll(); updateCount(); }); + binding.selectNone.setOnClickListener(v -> { adapter.selectNone(); updateCount(); }); + binding.selectInvert.setOnClickListener(v -> { adapter.selectInvert(); updateCount(); }); + binding.confirm.setOnClickListener(v -> { + if (callback != null) callback.onConfirm(new ArrayList<>(adapter.getCheckedKeys())); + dismiss(); + }); + } +} diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SearchSourceDialog.java b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SearchSourceDialog.java new file mode 100644 index 00000000000..ef35c362aab --- /dev/null +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SearchSourceDialog.java @@ -0,0 +1,70 @@ +package com.fongmi.android.tv.ui.dialog; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewbinding.ViewBinding; + +import com.fongmi.android.tv.databinding.DialogSearchSourceBinding; +import com.fongmi.android.tv.ui.adapter.SearchSourceAdapter; +import com.fongmi.android.tv.utils.SearchModeStore; +import com.fongmi.android.tv.utils.SearchSourceHelper; +import com.fongmi.android.tv.utils.SearchSourceItem; + +import java.util.ArrayList; +import java.util.List; + +public class SearchSourceDialog extends BaseBottomSheetDialog { + + private DialogSearchSourceBinding binding; + private SearchSourceAdapter adapter; + + public static SearchSourceDialog create() { + return new SearchSourceDialog(); + } + + public void show(Fragment fragment) { + for (Fragment f : fragment.getChildFragmentManager().getFragments()) if (f instanceof SearchSourceDialog) return; + show(fragment.getChildFragmentManager(), null); + } + + @Override + protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) { + return binding = DialogSearchSourceBinding.inflate(inflater, container, false); + } + + @Override + protected void initView() { + adapter = new SearchSourceAdapter(); + adapter.setItems(SearchSourceHelper.buildItems(), SearchModeStore.getMode()); + binding.recycler.setAdapter(adapter); + adapter.setOnConfigListener(this::openConfig); + adapter.setOnModeChangeListener(mode -> { + SearchModeStore.putMode(mode); + dismiss(); + }); + } + + private void openConfig(SearchSourceItem item) { + int mode = item.mode; + if (mode == SearchModeStore.MODE_WHITE || mode == SearchModeStore.MODE_BLACK) { + List initial = mode == SearchModeStore.MODE_WHITE + ? SearchModeStore.getWhiteList() : SearchModeStore.getBlackList(); + SearchSitePickerDialog.createSite(null, new ArrayList<>(initial), selected -> { + if (mode == SearchModeStore.MODE_WHITE) SearchModeStore.putWhiteList(new ArrayList<>(selected)); + else SearchModeStore.putBlackList(new ArrayList<>(selected)); + SearchModeStore.putMode(mode); + dismiss(); + }).show(this); + } else if (mode == SearchModeStore.MODE_TAG) { + SearchSitePickerDialog.createTag(new ArrayList<>(SearchModeStore.getTagGroups()), selected -> { + SearchModeStore.putTagGroups(new ArrayList<>(selected)); + SearchModeStore.putMode(mode); + dismiss(); + }).show(this); + } + } +} diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java index a2653a5aa87..877f4963b0e 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SiteDialog.java @@ -10,6 +10,8 @@ import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; +import android.widget.Toast; +import java.io.File; import androidx.annotation.NonNull; import androidx.appcompat.widget.LinearLayoutCompat; @@ -35,6 +37,7 @@ import com.fongmi.android.tv.utils.Util; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.github.catvod.utils.Path; import java.util.ArrayList; import java.util.List; @@ -278,6 +281,40 @@ public boolean onChangeLongClick(Site item) { return true; } + @Override + public void onDeleteClick(int position, Site item) { + new MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_WebHTV_LightDialog) + .setTitle(R.string.setting_site_delete_title) + .setMessage(getString(R.string.setting_site_delete_message, item.getName())) + .setPositiveButton(android.R.string.ok, (dialog, which) -> deleteFileSite(position, item)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void deleteFileSite(int position, Site item) { + String type = item.getFileType(); + String fileName = item.getFileName(); + if (fileName.isEmpty()) return; + String subDir = switch (type) { + case "XBPQ" -> "sites-json"; + case "JS" -> "sites-js/api"; + case "PY" -> "sites-py"; + case "RAW" -> "sites"; + default -> ""; + }; + if (subDir.isEmpty()) return; + File file = new File(Path.root() + "/tvbox/" + subDir, fileName); + boolean deleted = file.exists() && file.delete(); + if (!deleted && file.exists()) { + Toast.makeText(requireActivity(), R.string.remote_trust_delete_device_done, Toast.LENGTH_SHORT).show(); + return; + } + adapter.getItems().remove(position); + adapter.notifyItemRemoved(position); + adapter.notifyItemRangeChanged(position, adapter.getItemCount()); + Toast.makeText(requireActivity(), getString(R.string.setting_site_delete_done, item.getName()), Toast.LENGTH_SHORT).show(); + } + @Override public void onStart() { super.onStart(); diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SourceBlockDialog.java b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SourceBlockDialog.java new file mode 100644 index 00000000000..1b87521338c --- /dev/null +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/dialog/SourceBlockDialog.java @@ -0,0 +1,84 @@ +package com.fongmi.android.tv.ui.dialog; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewbinding.ViewBinding; + +import com.fongmi.android.tv.R; +import com.fongmi.android.tv.databinding.DialogSitePickerBinding; +import com.fongmi.android.tv.setting.Setting; +import com.fongmi.android.tv.setting.SourceBlockItem; +import com.fongmi.android.tv.ui.adapter.SourceCheckAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class SourceBlockDialog extends BaseBottomSheetDialog { + + private DialogSitePickerBinding binding; + private SourceCheckAdapter adapter; + private Callback callback; + + public interface Callback { + void onConfirm(); + } + + public static SourceBlockDialog create(Callback callback) { + SourceBlockDialog d = new SourceBlockDialog(); + d.callback = callback; + return d; + } + + public void show(Fragment parent) { + show(parent.getChildFragmentManager(), null); + } + + @Override + protected ViewBinding getBinding(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) { + return binding = DialogSitePickerBinding.inflate(inflater, container, false); + } + + @Override + protected void initView() { + binding.search.setVisibility(View.GONE); + binding.count.setVisibility(View.GONE); + adapter = new SourceCheckAdapter(); + binding.recycler.setAdapter(adapter); + adapter.setItems(buildSourceItems()); + } + + private List buildSourceItems() { + List items = new ArrayList<>(); + int[] sources = Setting.SOURCE_ALL; + String[] names = { + getString(R.string.source_vod_url), + getString(R.string.source_live_url), + getString(R.string.source_sites_json), + getString(R.string.source_sites_js), + getString(R.string.source_sites_py), + getString(R.string.source_sites_raw), + getString(R.string.source_lives_file) + }; + for (int i = 0; i < sources.length; i++) { + items.add(new SourceBlockItem(sources[i], names[i])); + } + return items; + } + + @Override + protected void initEvent() { + binding.selectAll.setOnClickListener(v -> adapter.selectAll()); + binding.selectNone.setOnClickListener(v -> adapter.selectNone()); + binding.selectInvert.setOnClickListener(v -> adapter.selectInvert()); + binding.confirm.setOnClickListener(v -> { + Setting.putSourceBlockMask(adapter.getMask()); + if (callback != null) callback.onConfirm(); + dismiss(); + }); + } +} diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/CollectFragment.java b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/CollectFragment.java index 0cdf4679b2e..9b109915eff 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/CollectFragment.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/CollectFragment.java @@ -40,6 +40,7 @@ import com.fongmi.android.tv.ui.custom.CustomScroller; import com.fongmi.android.tv.utils.MobileWindow; import com.fongmi.android.tv.utils.ResUtil; +import com.fongmi.android.tv.utils.SearchModeStore; import java.util.ArrayList; import java.util.List; @@ -148,13 +149,7 @@ private void setViewModel() { } private void setSites() { - String siteKey = getSiteKey(); - mSites = new ArrayList<>(); - for (Site site : VodConfig.get().getSites()) { - if (!site.isSearchable()) continue; - if (!TextUtils.isEmpty(siteKey) && !site.getKey().equals(siteKey)) continue; - mSites.add(site); - } + mSites = new ArrayList<>(SearchModeStore.filterSites(VodConfig.get().getSites(), getSiteKey())); SiteHealthStore.sortSites(mSites); } diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SearchFragment.java b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SearchFragment.java index 8a2917c40fb..45736745194 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SearchFragment.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SearchFragment.java @@ -34,7 +34,7 @@ import com.fongmi.android.tv.ui.adapter.WordAdapter; import com.fongmi.android.tv.ui.base.BaseFragment; import com.fongmi.android.tv.ui.custom.CustomTextListener; -import com.fongmi.android.tv.ui.dialog.SiteDialog; +import com.fongmi.android.tv.ui.dialog.SearchSourceDialog; import com.fongmi.android.tv.utils.SearchSuggest; import com.fongmi.android.tv.utils.Util; import com.github.catvod.net.OkHttp; @@ -280,7 +280,7 @@ private void onReset() { private void onSite() { Util.hideKeyboard(mBinding.keyword); - mBinding.keyword.post(() -> SiteDialog.create().search().show(this)); + mBinding.keyword.post(() -> SearchSourceDialog.create().show(this)); } private interface HotSaver { diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SettingEnhanceFragment.java b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SettingEnhanceFragment.java index 685497f7893..20a310013e4 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SettingEnhanceFragment.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/SettingEnhanceFragment.java @@ -32,6 +32,7 @@ import com.fongmi.android.tv.ui.dialog.RemoteTrustDialog; import com.fongmi.android.tv.ui.dialog.ShellProxyDialog; import com.fongmi.android.tv.ui.dialog.SiteHealthDialog; +import com.fongmi.android.tv.ui.dialog.SourceBlockDialog; import com.fongmi.android.tv.ui.dialog.ViewingRecordSyncDialog; import com.fongmi.android.tv.ui.dialog.WebHomeExtensionDialog; import com.fongmi.android.tv.utils.LoginStateSync; @@ -43,7 +44,7 @@ public class SettingEnhanceFragment extends BaseFragment { - private static final String URL_GITHUB = "https://github.com/fish2018/webhtv"; + private static final String URL_GITHUB = "https://github.com/llb0/webhtv"; private static final String URL_CNB = "https://cnb.cool/fish2035/ext"; private FragmentSettingEnhanceBinding mBinding; @@ -80,6 +81,7 @@ protected void initEvent() { } return false; }); + mBinding.fileSites.setOnClickListener(this::setFileSites); mBinding.driveCheck.setOnClickListener(this::setDriveCheck); mBinding.debugLog.setOnClickListener(this::setDebugLog); mBinding.siteHealthSort.setOnClickListener(view -> SiteHealthDialog.show(this, this::setText)); @@ -108,6 +110,7 @@ protected void initEvent() { private void reorderItems() { ViewGroup parent = (ViewGroup) mBinding.customCsp.getParent(); View[] order = { + mBinding.fileSites, mBinding.customCsp, mBinding.webHomeExtension, mBinding.gitCloud, @@ -131,6 +134,7 @@ private void reorderItems() { private void setText() { if (!canSetText()) return; + safeSet("fileSites", mBinding.fileSitesText, () -> getString(R.string.setting_source_block_count, Setting.getSourceBlockedCount(), Setting.SOURCE_ALL.length)); safeSet("driveCheck", mBinding.driveCheckText, () -> getSwitch(Setting.isDriveCheck())); safeSet("debugLog", mBinding.debugLogText, () -> getSwitch(Setting.isDebugLog())); safeSet("siteHealthSort", mBinding.siteHealthSortText, () -> getSwitch(Setting.isSiteHealthSort())); @@ -206,6 +210,10 @@ private interface TextSupplier { CharSequence get(); } + private void setFileSites(View view) { + SourceBlockDialog.create(this::setText).show(this); + } + private void setDriveCheck(View view) { Setting.putDriveCheck(!Setting.isDriveCheck()); mBinding.driveCheckText.setText(getSwitch(Setting.isDriveCheck())); @@ -282,4 +290,4 @@ public void onDestroyView() { super.onDestroyView(); mBinding = null; } -} +} \ No newline at end of file diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/VodFragment.java b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/VodFragment.java index cb31fcad28e..a72bf6945f7 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/VodFragment.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/fragment/VodFragment.java @@ -48,6 +48,7 @@ import com.fongmi.android.tv.ui.adapter.TypeAdapter; import com.fongmi.android.tv.ui.base.BaseFragment; import com.fongmi.android.tv.ui.dialog.ApkPushDialog; +import com.fongmi.android.tv.ui.dialog.BackupProgressDialog; import com.fongmi.android.tv.ui.dialog.FilterDialog; import com.fongmi.android.tv.ui.dialog.HistoryDialog; import com.fongmi.android.tv.ui.dialog.LinkDialog; @@ -55,6 +56,7 @@ import com.fongmi.android.tv.ui.dialog.ReceiveDialog; import com.fongmi.android.tv.ui.dialog.SiteDialog; import com.fongmi.android.tv.ui.dialog.TypeDialog; +import com.fongmi.android.tv.utils.ConfigDownload; import com.fongmi.android.tv.utils.ImgUtil; import com.fongmi.android.tv.utils.Notify; import com.fongmi.android.tv.utils.ResUtil; @@ -254,27 +256,9 @@ private void onSite(View view) { } private boolean reloadConfig(View view) { - VodConfig.get().clear().config(getConfig()).load(new Callback() { - @Override - public void start() { - showProgress(); - hideContent(); - } - - @Override - public void success() { - hideProgress(); - showContent(); - } - - @Override - public void error(String msg) { - Notify.dismiss(); - Notify.show(msg); - hideProgress(); - showContent(); - } - }); + HomeActivity activity = homeActivity(); + if (activity != null) activity.initConfig(); + else homeContent(); return true; } @@ -284,13 +268,14 @@ private void onFilter(View view) { private boolean onMenuItemClick(MenuItem item) { if (item.getItemId() == R.id.refresh) { - if (mWeb != null && mWeb.isVisible()) mWeb.reload(); - else homeContent(); + HomeActivity activity = homeActivity(); + if (activity != null) activity.initConfig(); } else if (item.getItemId() == R.id.keep) KeepActivity.start(requireActivity()); else if (item.getItemId() == R.id.search) SearchActivity.start(requireActivity()); else if (item.getItemId() == R.id.history) HistoryActivity.start(requireActivity()); else if (item.getItemId() == R.id.sync) OneKeySyncDialog.create().show(requireActivity()); else if (item.getItemId() == R.id.push_apk) ApkPushDialog.create().listener(this::onApkDeviceSelected).show(requireActivity()); + else if (item.getItemId() == R.id.download_config) downloadConfig(); else if (item.getItemId() == R.id.enhance && homeActivity() != null) homeActivity().openEnhanceFromVod(); else if (item.getItemId() == R.id.web_home_fullscreen) onWebHomeFullscreen(); else return false; @@ -322,6 +307,34 @@ private void updateToolbarMenu() { Menu menu = mBinding.toolbar.getMenu(); MenuItem fullscreen = menu.findItem(R.id.web_home_fullscreen); if (fullscreen != null) fullscreen.setVisible(Setting.isWebHomeFullscreen() && mWeb != null && mWeb.isVisible()); + MenuItem download = menu.findItem(R.id.download_config); + if (download != null) download.setVisible(ConfigDownload.shouldShow(VodConfig.getUrl())); + } + + private void downloadConfig() { + String url = VodConfig.getUrl(); + if (!ConfigDownload.shouldShow(url)) return; + String title = getString(R.string.menu_download_config); + BackupProgressDialog dialog = BackupProgressDialog.open(getChildFragmentManager(), title); + ConfigDownload.start(url, new ConfigDownload.Listener() { + @Override + public void onProgress(String stage, int current, int total) { + int percent = total <= 0 ? 0 : Math.min(100, current * 100 / total); + dialog.update(stage, percent, current, total); + } + @Override + public void onSuccess(String dirName) { + dialog.finish(); + if (!isAdded()) return; + Notify.show(getString(R.string.config_download_success, dirName)); + } + @Override + public void onError(String msg) { + dialog.finish(); + if (!isAdded()) return; + Notify.show(getString(R.string.config_download_failed, msg)); + } + }); } private void setSearchLongClick() { diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodListHolder.java b/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodListHolder.java index e7590021d02..a225955018f 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodListHolder.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodListHolder.java @@ -4,6 +4,7 @@ import com.bumptech.glide.Glide; import com.fongmi.android.tv.bean.Vod; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.databinding.AdapterVodListBinding; import com.fongmi.android.tv.ui.adapter.VodAdapter; import com.fongmi.android.tv.ui.base.BaseVodHolder; @@ -23,6 +24,7 @@ public VodListHolder(@NonNull AdapterVodListBinding binding, VodAdapter.OnClickL @Override public void initView(Vod item) { binding.name.setText(item.getName()); + binding.name.setMaxLines(Setting.resolveTitleMaxLines()); binding.remark.setText(item.getRemarks()); binding.name.setVisibility(item.getNameVisible()); binding.remark.setVisibility(item.getRemarkVisible()); diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java b/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java index 1edf430000a..42b49c23fdb 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodOvalHolder.java @@ -4,6 +4,7 @@ import com.bumptech.glide.Glide; import com.fongmi.android.tv.bean.Vod; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.databinding.AdapterVodOvalBinding; import com.fongmi.android.tv.ui.adapter.VodAdapter; import com.fongmi.android.tv.ui.base.BaseVodHolder; @@ -29,6 +30,7 @@ public VodOvalHolder size(int[] size) { @Override public void initView(Vod item) { binding.name.setText(item.getName()); + binding.name.setMaxLines(Setting.resolveTitleMaxLines()); binding.name.setVisibility(item.getNameVisible()); binding.getRoot().setOnClickListener(v -> listener.onItemClick(item)); binding.getRoot().setOnLongClickListener(v -> listener.onLongClick(item)); diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java b/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java index e7ac43bd4b0..df1b04c9c28 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/holder/VodRectHolder.java @@ -4,6 +4,7 @@ import com.bumptech.glide.Glide; import com.fongmi.android.tv.bean.Vod; +import com.fongmi.android.tv.setting.Setting; import com.fongmi.android.tv.databinding.AdapterVodRectBinding; import com.fongmi.android.tv.ui.adapter.VodAdapter; import com.fongmi.android.tv.ui.base.BaseVodHolder; @@ -29,6 +30,7 @@ public VodRectHolder size(int[] size) { @Override public void initView(Vod item) { binding.name.setText(item.getName()); + binding.name.setMaxLines(Setting.resolveTitleMaxLines()); binding.year.setText(item.getYear()); binding.site.setText(item.getSiteName()); binding.remark.setText(item.getRemarks()); diff --git a/app/src/mobile/res/drawable/ic_site_delete.xml b/app/src/mobile/res/drawable/ic_site_delete.xml new file mode 100644 index 00000000000..79372b1d6e0 --- /dev/null +++ b/app/src/mobile/res/drawable/ic_site_delete.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/mobile/res/layout/adapter_site.xml b/app/src/mobile/res/layout/adapter_site.xml index 7c23d6585cc..714400eadfe 100644 --- a/app/src/mobile/res/layout/adapter_site.xml +++ b/app/src/mobile/res/layout/adapter_site.xml @@ -37,6 +37,15 @@ android:src="@drawable/ic_site_search" app:tint="@color/dialog_outlined_button_text" /> + + - + \ No newline at end of file diff --git a/app/src/mobile/res/layout/dialog_device.xml b/app/src/mobile/res/layout/dialog_device.xml index 866d9243117..9396c1b0c17 100644 --- a/app/src/mobile/res/layout/dialog_device.xml +++ b/app/src/mobile/res/layout/dialog_device.xml @@ -21,7 +21,7 @@ android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/device_select" - android:textColor="#202124" + android:textColor="?attr/colorOnSurface" android:textSize="16sp" /> + diff --git a/app/src/mobile/res/layout/dialog_site_picker.xml b/app/src/mobile/res/layout/dialog_site_picker.xml new file mode 100644 index 00000000000..bc1edfefc16 --- /dev/null +++ b/app/src/mobile/res/layout/dialog_site_picker.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/mobile/res/layout/fragment_setting_enhance.xml b/app/src/mobile/res/layout/fragment_setting_enhance.xml index 2ea095df2de..38945b9ed1b 100644 --- a/app/src/mobile/res/layout/fragment_setting_enhance.xml +++ b/app/src/mobile/res/layout/fragment_setting_enhance.xml @@ -30,6 +30,33 @@ android:paddingEnd="16dp" android:paddingBottom="16dp"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/mobile/res/layout/item_site_check.xml b/app/src/mobile/res/layout/item_site_check.xml new file mode 100644 index 00000000000..c20bdf99a89 --- /dev/null +++ b/app/src/mobile/res/layout/item_site_check.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/mobile/res/menu/menu_vod.xml b/app/src/mobile/res/menu/menu_vod.xml index 658c44e1620..e89c4563e72 100644 --- a/app/src/mobile/res/menu/menu_vod.xml +++ b/app/src/mobile/res/menu/menu_vod.xml @@ -52,6 +52,11 @@ android:icon="@drawable/ic_popup_apk" android:title="@string/menu_push_apk" /> + + 站点 重置 推送apk + 接口下载 + 开始下载接口… + 接口已下载到 tvbox/api/%1$s/ + 接口下载失败:%1$s 正在准备APK… 推送apk · 选择接收设备 推送apk · %1$s · %2$s diff --git a/app/src/mobile/res/values-zh-rTW/strings.xml b/app/src/mobile/res/values-zh-rTW/strings.xml index 7b6d9f3643f..4685f8ae75c 100644 --- a/app/src/mobile/res/values-zh-rTW/strings.xml +++ b/app/src/mobile/res/values-zh-rTW/strings.xml @@ -16,6 +16,10 @@ 站點 重設 推送apk + 接口下載 + 開始下載接口… + 接口已下載到 tvbox/api/%1$s/ + 接口下載失敗:%1$s 正在準備APK… 推送apk · 選擇接收裝置 推送apk · %1$s · %2$s diff --git a/app/src/mobile/res/values/strings.xml b/app/src/mobile/res/values/strings.xml index 6cf10f03507..74c65c6c26c 100644 --- a/app/src/mobile/res/values/strings.xml +++ b/app/src/mobile/res/values/strings.xml @@ -16,6 +16,10 @@ Site Reset Push APK + Download Config + Downloading config… + Config downloaded to tvbox/api/%1$s/ + Config download failed: %1$s Preparing APK... Push APK · Select receiving device Push APK · %1$s · %2$s diff --git a/catvod/src/main/java/com/github/catvod/utils/Path.java b/catvod/src/main/java/com/github/catvod/utils/Path.java index 2c20efd66f0..1a2199d2961 100644 --- a/catvod/src/main/java/com/github/catvod/utils/Path.java +++ b/catvod/src/main/java/com/github/catvod/utils/Path.java @@ -27,7 +27,9 @@ private static File mkdir(File file) { } public static boolean exists(String path) { - return new File(path.replace("file://", "")).exists(); + if (path == null) return false; + File file = local(path); + return file != null && file.exists(); } public static boolean exists(File file) { @@ -127,7 +129,8 @@ public static File thunder(String name) { } public static File local(String path) { - path = path.replace("file:/", ""); + if (path == null) return null; + path = path.replace("clan://", "file://tvbox/").replace("file:/", ""); File file = new File(root(), path); return file.exists() ? file : new File(path); } diff --git a/chaquo/src/main/java/com/fongmi/chaquo/Loader.java b/chaquo/src/main/java/com/fongmi/chaquo/Loader.java index 8e3f0f674ed..2d7f1223244 100644 --- a/chaquo/src/main/java/com/fongmi/chaquo/Loader.java +++ b/chaquo/src/main/java/com/fongmi/chaquo/Loader.java @@ -30,19 +30,32 @@ public Spider spider(String api) { } private String source(String api, String name) { - if (!api.startsWith("http")) return api; - File cache = Path.py(name); - try (Response response = OkHttp.newCall(OkHttp.client(15000), api).execute()) { - if (!response.isSuccessful()) throw new IllegalStateException("HTTP " + response.code()); - String source = response.body().string(); - if (TextUtils.isEmpty(source)) throw new IllegalStateException("Empty python script"); - Path.write(cache, source.getBytes(StandardCharsets.UTF_8)); - return source; - } catch (Exception e) { - String cached = Path.read(cache); - if (!TextUtils.isEmpty(cached)) return cached; - throw new IllegalStateException("Unable to download python script: " + api, e); + if (api.startsWith("http")) { + File cache = Path.py(name); + try (Response response = OkHttp.newCall(OkHttp.client(15000), api).execute()) { + if (!response.isSuccessful()) throw new IllegalStateException("HTTP " + response.code()); + String source = response.body().string(); + if (TextUtils.isEmpty(source)) throw new IllegalStateException("Empty python script"); + Path.write(cache, source.getBytes(StandardCharsets.UTF_8)); + return source; + } catch (Exception e) { + String cached = Path.read(cache); + if (!TextUtils.isEmpty(cached)) return cached; + throw new IllegalStateException("Unable to download python script: " + api, e); + } } + File local = local(api); + if (local != null) return Path.read(local); + return api; + } + + private File local(String api) { + if (api == null) return null; + api = api.replace("clan://", "file://tvbox/"); + if (api.startsWith("file://")) return new File(api.replace("file://", "")); + if (api.startsWith("file:/")) return new File(api.replace("file:/", "")); + if (api.startsWith("/")) return new File(api); + return null; } private String scriptName(String api) { @@ -50,4 +63,4 @@ private String scriptName(String api) { String suffix = lower.contains(".py") ? ".py" : ".script"; return Util.md5(api) + suffix; } -} +} \ No newline at end of file diff --git a/chaquo/src/main/python/base/spider.py b/chaquo/src/main/python/base/spider.py index a6b8c2a6bc8..7a35c552732 100644 --- a/chaquo/src/main/python/base/spider.py +++ b/chaquo/src/main/python/base/spider.py @@ -8,7 +8,7 @@ from com.chaquo.python import Python from abc import abstractmethod, ABCMeta from importlib.machinery import SourceFileLoader - +from com.github.catvod.crawler import SpiderDebug class Spider(metaclass=ABCMeta): _instance = None @@ -116,9 +116,11 @@ def getProxyUrl(self, local=True): def log(self, msg): if isinstance(msg, dict) or isinstance(msg, list): - print(json.dumps(msg, ensure_ascii=False)) + text = json.dumps(msg, ensure_ascii=False) else: - print(f'{msg}') + text = str(msg) + SpiderDebug.log("python-spider", text) + print(text) def getCache(self, key): value = self.fetch(f'http://127.0.0.1:{Proxy.getPort()}/cache?do=get&key={key}', timeout=5).text diff --git a/quickjs/src/main/java/com/fongmi/quickjs/method/Console.java b/quickjs/src/main/java/com/fongmi/quickjs/method/Console.java index fc1cc63aefb..7847435b077 100644 --- a/quickjs/src/main/java/com/fongmi/quickjs/method/Console.java +++ b/quickjs/src/main/java/com/fongmi/quickjs/method/Console.java @@ -2,6 +2,7 @@ import com.orhanobut.logger.Logger; import com.whl.quickjs.wrapper.QuickJSContext; +import com.github.catvod.crawler.SpiderDebug; public class Console implements QuickJSContext.Console { @@ -10,20 +11,24 @@ public class Console implements QuickJSContext.Console { @Override public void log(String info) { Logger.t(TAG).d(info); + SpiderDebug.log("quickjs", info); } @Override public void info(String info) { Logger.t(TAG).i(info); + SpiderDebug.log("quickjs", info); } @Override public void warn(String info) { Logger.t(TAG).w(info); + SpiderDebug.log("quickjs", info); } @Override public void error(String info) { Logger.t(TAG).e(info); + SpiderDebug.log("quickjs", info); } } \ No newline at end of file diff --git a/quickjs/src/main/java/com/fongmi/quickjs/utils/Module.java b/quickjs/src/main/java/com/fongmi/quickjs/utils/Module.java index 001f74d8cac..c423d6496bc 100644 --- a/quickjs/src/main/java/com/fongmi/quickjs/utils/Module.java +++ b/quickjs/src/main/java/com/fongmi/quickjs/utils/Module.java @@ -5,6 +5,9 @@ import com.github.catvod.net.OkHttp; import com.github.catvod.utils.Asset; +import com.github.catvod.utils.Path; + +import java.io.File; public class Module { @@ -20,14 +23,30 @@ public static Module get() { } public String fetch(String name) { + if (name == null) return null; + name = name.replace("clan://", "file://tvbox/"); String content = cache.get(name); if (!TextUtils.isEmpty(content)) return content; if (name.startsWith("http")) cache.put(name, content = OkHttp.string(name)); else if (name.startsWith("assets")) cache.put(name, content = Asset.read(name)); else if (name.startsWith("lib/")) cache.put(name, content = Asset.read("js/" + name)); + else if (name.startsWith("/")) cache.put(name, content = Path.read(new File(name))); + else if (name.startsWith("file")) cache.put(name, content = Path.read(Path.local(name))); + else if (looksLikeSource(name)) content = name; return content; } + private boolean looksLikeSource(String text) { + if (TextUtils.isEmpty(text)) return false; + return text.contains("function ") + || text.contains("const ") + || text.contains("let ") + || text.contains("var ") + || text.contains("=>") + || text.contains("import ") + || text.contains("__jsEvalReturn"); + } + public void clear() { cache.evictAll(); }