feat(YouTube - Gemini): Redesign, support multiple API keys, add Ask about this video
Close https://github.com/anddea/revanced-patches/issues/1419 Close https://github.com/anddea/revanced-patches/issues/1453
This commit is contained in:
parent
84bfbc7bcd
commit
374b0e55b9
9 changed files with 2422 additions and 610 deletions
|
|
@ -10,6 +10,7 @@ import android.app.Dialog;
|
|||
import android.content.Context;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
|
|
@ -58,21 +59,13 @@ public class SheetBottomDialog {
|
|||
// Create wrapper layout for side margins.
|
||||
LinearLayout wrapperLayout = new LinearLayout(context);
|
||||
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
wrapperLayout.setGravity(Gravity.BOTTOM);
|
||||
|
||||
// Create drag container.
|
||||
DraggableLinearLayout dragContainer = new DraggableLinearLayout(context, animationDuration);
|
||||
dragContainer.setOrientation(LinearLayout.VERTICAL);
|
||||
dragContainer.setDialog(dialog);
|
||||
|
||||
// Add top spacer.
|
||||
View spacer = new View(context);
|
||||
final int dip40 = dipToPixels(40);
|
||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, dip40);
|
||||
spacer.setLayoutParams(spacerParams);
|
||||
spacer.setClickable(true);
|
||||
dragContainer.addView(spacer);
|
||||
|
||||
// Add content view.
|
||||
ViewGroup parent = (ViewGroup) contentView.getParent();
|
||||
if (parent != null) parent.removeView(contentView);
|
||||
|
|
@ -173,6 +166,8 @@ public class SheetBottomDialog {
|
|||
|
||||
private SlideDialog dialog;
|
||||
private float dismissThreshold;
|
||||
@Nullable
|
||||
private View touchedScrollableChild;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link DraggableLinearLayout} with the specified context.
|
||||
|
|
@ -223,23 +218,26 @@ public class SheetBottomDialog {
|
|||
*/
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
if (!isDragEnabled) return false;
|
||||
if (dialog == null || !isDragEnabled) return false;
|
||||
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
initialTouchRawY = ev.getRawY();
|
||||
float initialTouchX = ev.getX();
|
||||
float initialTouchY = ev.getY();
|
||||
isDragging = false;
|
||||
scroller.forceFinished(true);
|
||||
removeCallbacks(settleRunnable);
|
||||
velocityTracker.clear();
|
||||
velocityTracker.addMovement(ev);
|
||||
dragOffset = getTranslationY();
|
||||
touchedScrollableChild = findScrollableChildUnder(this, (int) initialTouchX, (int) initialTouchY);
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
float dy = ev.getRawY() - initialTouchRawY;
|
||||
if (dy > ViewConfiguration.get(getContext()).getScaledTouchSlop()
|
||||
&& !canChildScrollUp()) {
|
||||
&& canStartDrag()) {
|
||||
isDragging = true;
|
||||
return true; // Intercept touches for drag.
|
||||
}
|
||||
|
|
@ -255,7 +253,7 @@ public class SheetBottomDialog {
|
|||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (!isDragEnabled) return super.onTouchEvent(ev);
|
||||
if (dialog == null || !isDragEnabled) return super.onTouchEvent(ev);
|
||||
velocityTracker.addMovement(ev);
|
||||
|
||||
switch (ev.getActionMasked()) {
|
||||
|
|
@ -327,28 +325,45 @@ public class SheetBottomDialog {
|
|||
*
|
||||
* @return True if a child can scroll upward, false otherwise.
|
||||
*/
|
||||
private boolean canChildScrollUp() {
|
||||
View target = findScrollableChild(this);
|
||||
return target != null && target.canScrollVertically(-1);
|
||||
private boolean canStartDrag() {
|
||||
return touchedScrollableChild == null || !touchedScrollableChild.canScrollVertically(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively searches for a scrollable child view within the given view group.
|
||||
* Recursively searches for a scrollable child under the given touch point.
|
||||
*
|
||||
* @param group The view group to search.
|
||||
* @return The scrollable child view, or null if none found.
|
||||
* @param x The X coordinate relative to the current group.
|
||||
* @param y The Y coordinate relative to the current group.
|
||||
* @return The scrollable child view under the touch point, or null if none found.
|
||||
*/
|
||||
private View findScrollableChild(ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
@Nullable
|
||||
private View findScrollableChildUnder(@NonNull ViewGroup group, int x, int y) {
|
||||
for (int i = group.getChildCount() - 1; i >= 0; i--) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child.canScrollVertically(-1)) return child;
|
||||
if (child.getVisibility() != View.VISIBLE || !isPointInsideChild(child, x, y)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int childX = x - child.getLeft() + child.getScrollX();
|
||||
int childY = y - child.getTop() + child.getScrollY();
|
||||
|
||||
if (child instanceof ViewGroup) {
|
||||
View scroll = findScrollableChild((ViewGroup) child);
|
||||
View scroll = findScrollableChildUnder((ViewGroup) child, childX, childY);
|
||||
if (scroll != null) return scroll;
|
||||
}
|
||||
|
||||
if (child.canScrollVertically(-1) || child.canScrollVertically(1)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isPointInsideChild(@NonNull View child, int x, int y) {
|
||||
Rect bounds = new Rect(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
|
||||
return bounds.contains(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -41,7 +41,6 @@
|
|||
package app.morphe.extension.youtube.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
|
|
@ -51,19 +50,15 @@ import android.text.SpannableStringBuilder;
|
|||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.morphe.extension.shared.settings.AppLanguage;
|
||||
import app.morphe.extension.shared.ui.SheetBottomDialog;
|
||||
import app.morphe.extension.shared.utils.IntentUtils;
|
||||
import app.morphe.extension.shared.utils.Logger;
|
||||
import app.morphe.extension.youtube.settings.Settings;
|
||||
|
|
@ -87,7 +82,6 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
import static app.morphe.extension.shared.utils.StringRef.str;
|
||||
import static app.morphe.extension.shared.utils.Utils.dipToPixels;
|
||||
import static app.morphe.extension.shared.utils.Utils.showToastLong;
|
||||
import static app.morphe.extension.shared.utils.Utils.showToastShort;
|
||||
|
||||
|
|
@ -192,10 +186,28 @@ public final class GeminiManager {
|
|||
private final ExecutorService metadataExecutor = Executors.newCachedThreadPool();
|
||||
|
||||
/**
|
||||
* Reference to the progress dialog.
|
||||
* Active loading bottom sheet for summarize/transcribe operations.
|
||||
*/
|
||||
@Nullable
|
||||
private WeakReference<AlertDialog> progressDialogRef;
|
||||
private GeminiBottomSheetUi.LoadingSheet loadingSheet;
|
||||
|
||||
/**
|
||||
* Active summary bottom sheet used after summarization completes.
|
||||
*/
|
||||
@Nullable
|
||||
private GeminiBottomSheetUi.SummarySheet summarySheet;
|
||||
|
||||
/**
|
||||
* Active transcription bottom sheet used after Gemini transcription completes.
|
||||
*/
|
||||
@Nullable
|
||||
private WeakReference<SheetBottomDialog.SlideDialog> transcriptionDialogRef;
|
||||
|
||||
/**
|
||||
* Active follow-up chat task inside the summary bottom sheet.
|
||||
*/
|
||||
@Nullable
|
||||
private Future<?> activeSummaryChatTask;
|
||||
|
||||
/**
|
||||
* Flag indicating if the progress dialog is minimized.
|
||||
|
|
@ -275,6 +287,11 @@ public final class GeminiManager {
|
|||
*/
|
||||
private final Map<String, String> summaryModelCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Cache for per-video summary follow-up chat history.
|
||||
*/
|
||||
private final Map<String, List<GeminiUtils.ChatMessage>> summaryConversationCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Cache for parsed transcriptions.
|
||||
*/
|
||||
|
|
@ -405,19 +422,27 @@ public final class GeminiManager {
|
|||
|
||||
prepareForNewOperationInternal(OperationType.SUMMARIZE, videoUrl);
|
||||
|
||||
final String apiKey = Settings.GEMINI_API_KEY.get();
|
||||
if (isEmptyApiKey(apiKey)) {
|
||||
final List<String> apiKeys = GeminiUtils.parseApiKeys(Settings.GEMINI_API_KEY.get());
|
||||
if (isEmptyApiKey(apiKeys)) {
|
||||
resetOperationStateInternal(OperationType.SUMMARIZE, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Starting new summarization workflow: " + videoId);
|
||||
activeTasks.put(taskKey, DUMMY_FUTURE);
|
||||
taskStartTimes.put(taskKey, System.currentTimeMillis());
|
||||
|
||||
showProgressDialogInternal(context, OperationType.SUMMARIZE);
|
||||
|
||||
GeminiUtils.getVideoSummary(videoUrl, apiKey, new GeminiUtils.Callback() {
|
||||
Future<?> summaryTask = GeminiUtils.getVideoSummary(videoUrl, apiKeys, new GeminiUtils.Callback() {
|
||||
@Override
|
||||
public void onPartial(String partialText, String accumulatedText, @Nullable String model) {
|
||||
if (!activeTasks.containsKey(taskKey)) {
|
||||
Logger.printDebug(() -> "Summary partial callback ignored - task was cancelled: " + taskKey);
|
||||
return;
|
||||
}
|
||||
ensureMainThread(() -> handleSummaryStreamUpdate(videoUrl, accumulatedText));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
onSuccessWithModel(result, null);
|
||||
|
|
@ -443,6 +468,7 @@ public final class GeminiManager {
|
|||
handleApiResponseInternal(context, OperationType.SUMMARIZE, videoUrl, null, error);
|
||||
}
|
||||
});
|
||||
activeTasks.put(taskKey, summaryTask != null ? summaryTask : DUMMY_FUTURE);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -605,8 +631,8 @@ public final class GeminiManager {
|
|||
updateTimerMessageInternal();
|
||||
}
|
||||
|
||||
final String apiKey = Settings.GEMINI_API_KEY.get();
|
||||
if (isEmptyApiKey(apiKey)) {
|
||||
final List<String> apiKeys = GeminiUtils.parseApiKeys(Settings.GEMINI_API_KEY.get());
|
||||
if (isEmptyApiKey(apiKeys)) {
|
||||
activeTasks.remove(taskKey);
|
||||
taskStartTimes.remove(taskKey);
|
||||
if (Objects.equals(currentVideoUrl, videoUrl)) {
|
||||
|
|
@ -618,10 +644,10 @@ public final class GeminiManager {
|
|||
// Call Gemini for translation
|
||||
String cleanedJson = YandexVotUtils.stripTokensFromYandexJson(rawIntermediateJson);
|
||||
|
||||
GeminiUtils.translateYandexJson(
|
||||
Future<?> translationTask = GeminiUtils.translateYandexJson(
|
||||
cleanedJson,
|
||||
targetLangName,
|
||||
apiKey,
|
||||
apiKeys,
|
||||
new GeminiUtils.Callback() {
|
||||
@Override
|
||||
public void onSuccess(String translatedJson) {
|
||||
|
|
@ -644,6 +670,7 @@ public final class GeminiManager {
|
|||
}
|
||||
}
|
||||
);
|
||||
activeTasks.put(taskKey, translationTask != null ? translationTask : DUMMY_FUTURE);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -697,11 +724,9 @@ public final class GeminiManager {
|
|||
isWaitingForYandexRetry = true;
|
||||
baseLoadingMessage = str("revanced_gemini_loading_yandex_transcribe") + "\n(" + statusMessage + ")";
|
||||
|
||||
AlertDialog currentDialog = (progressDialogRef != null) ? progressDialogRef.get() : null;
|
||||
|
||||
if (currentDialog != null && currentDialog.isShowing() && !isProgressDialogMinimized) {
|
||||
if (loadingSheet != null && loadingSheet.isShowing() && !isProgressDialogMinimized) {
|
||||
updateTimerMessageInternal();
|
||||
} else if (progressDialogRef == null && !isProgressDialogMinimized) {
|
||||
} else if (loadingSheet == null && !isProgressDialogMinimized) {
|
||||
showProgressDialogInternal(context, OperationType.TRANSCRIBE);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -889,8 +914,8 @@ public final class GeminiManager {
|
|||
Logger.printInfo(() -> "Starting new Gemini direct transcription workflow: " + videoId);
|
||||
String taskKey = getTaskKey(videoId, OperationType.TRANSCRIBE);
|
||||
|
||||
final String apiKey = Settings.GEMINI_API_KEY.get();
|
||||
if (isEmptyApiKey(apiKey)) {
|
||||
final List<String> apiKeys = GeminiUtils.parseApiKeys(Settings.GEMINI_API_KEY.get());
|
||||
if (isEmptyApiKey(apiKeys)) {
|
||||
resetOperationStateInternal(OperationType.TRANSCRIBE, true);
|
||||
taskStartTimes.remove(taskKey);
|
||||
return;
|
||||
|
|
@ -898,7 +923,7 @@ public final class GeminiManager {
|
|||
|
||||
showProgressDialogInternal(context, OperationType.TRANSCRIBE);
|
||||
|
||||
GeminiUtils.getVideoTranscription(videoUrl, apiKey, new GeminiUtils.Callback() {
|
||||
Future<?> transcriptionTask = GeminiUtils.getVideoTranscription(videoUrl, apiKeys, new GeminiUtils.Callback() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
if (!activeTasks.containsKey(taskKey)) {
|
||||
|
|
@ -919,6 +944,7 @@ public final class GeminiManager {
|
|||
handleApiResponseInternal(context, OperationType.TRANSCRIBE, videoUrl, null, error);
|
||||
}
|
||||
});
|
||||
activeTasks.put(taskKey, transcriptionTask != null ? transcriptionTask : DUMMY_FUTURE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -962,6 +988,9 @@ public final class GeminiManager {
|
|||
private void prepareForNewOperationInternal(@NonNull OperationType newOperationType, @NonNull String newVideoUrl) {
|
||||
Logger.printDebug(() -> "Preparing UI for operation: " + newOperationType + " for URL: " + newVideoUrl);
|
||||
|
||||
dismissSummarySheetInternal();
|
||||
dismissTranscriptionDialogInternal();
|
||||
|
||||
isCancelled = false;
|
||||
isProgressDialogMinimized = false;
|
||||
isWaitingForYandexRetry = false;
|
||||
|
|
@ -1017,6 +1046,7 @@ public final class GeminiManager {
|
|||
if (opType == OperationType.SUMMARIZE) {
|
||||
summaryCache.put(videoId, result);
|
||||
summaryTimeCache.put(videoId, time);
|
||||
summaryConversationCache.remove(videoId);
|
||||
if (!TextUtils.isEmpty(usedModel)) {
|
||||
summaryModelCache.put(videoId, usedModel);
|
||||
} else {
|
||||
|
|
@ -1230,11 +1260,11 @@ public final class GeminiManager {
|
|||
/**
|
||||
* Checks if the API key is empty and shows a toast if so.
|
||||
*
|
||||
* @param key The API key.
|
||||
* @param apiKeys List of API keys.
|
||||
* @return True if the key is empty, false otherwise.
|
||||
*/
|
||||
private boolean isEmptyApiKey(@Nullable String key) {
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
private boolean isEmptyApiKey(@Nullable List<String> apiKeys) {
|
||||
if (apiKeys == null || apiKeys.isEmpty()) {
|
||||
showToastLong(str("revanced_gemini_error_no_api_key"));
|
||||
Logger.printDebug(() -> "isValidApiKey: API key is empty or null.");
|
||||
return true;
|
||||
|
|
@ -1332,6 +1362,170 @@ public final class GeminiManager {
|
|||
return "English";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getVideoMetaText(@Nullable String videoUrl) {
|
||||
if (TextUtils.isEmpty(videoUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String videoId = getVideoIdFromUrl(videoUrl);
|
||||
String meta = videoMetadataCache.getOrDefault(videoId, "");
|
||||
if (!TextUtils.isEmpty(meta) && !meta.equals(str("revanced_gemini_loading_default"))) {
|
||||
return meta;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getCurrentVideoMetaText() {
|
||||
return getVideoMetaText(currentVideoUrl);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String buildInfoText(int seconds, @Nullable String usedModel) {
|
||||
StringBuilder infoText = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(usedModel)) {
|
||||
infoText.append(usedModel);
|
||||
}
|
||||
if (seconds >= 0) {
|
||||
if (infoText.length() > 0) {
|
||||
infoText.append('\n');
|
||||
}
|
||||
infoText.append(str("revanced_gemini_time_taken", seconds));
|
||||
}
|
||||
return infoText.length() == 0 ? null : infoText.toString();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void handleSummaryStreamUpdate(@NonNull String videoUrl, @NonNull String accumulatedText) {
|
||||
if (!Objects.equals(currentVideoUrl, videoUrl) || isProgressDialogMinimized || loadingSheet == null || !loadingSheet.isShowing()) {
|
||||
return;
|
||||
}
|
||||
loadingSheet.updatePreview(accumulatedText);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void handleSummaryChatSend(
|
||||
@NonNull String videoUrl,
|
||||
@NonNull String summary,
|
||||
@NonNull GeminiBottomSheetUi.SummarySheet sheet
|
||||
) {
|
||||
if (summarySheet != sheet || !sheet.isShowing()) {
|
||||
sheet.failPendingResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> apiKeys = GeminiUtils.parseApiKeys(Settings.GEMINI_API_KEY.get());
|
||||
if (isEmptyApiKey(apiKeys)) {
|
||||
sheet.failPendingResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
cancelActiveSummaryChatTaskInternal();
|
||||
|
||||
Future<?> chatTask = GeminiUtils.chatWithVideo(
|
||||
videoUrl,
|
||||
summary,
|
||||
sheet.getConversationWithPendingQuestion(),
|
||||
apiKeys,
|
||||
new GeminiUtils.Callback() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
onSuccessWithModel(result, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartial(String partialText, String accumulatedText, @Nullable String model) {
|
||||
ensureMainThread(() -> {
|
||||
if (summarySheet == sheet && sheet.isShowing()) {
|
||||
sheet.updatePendingResponse(accumulatedText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccessWithModel(String result, @Nullable String model) {
|
||||
ensureMainThread(() -> {
|
||||
if (summarySheet == sheet && sheet.isShowing()) {
|
||||
sheet.commitPendingResponse(normalizeGeminiText(result));
|
||||
}
|
||||
activeSummaryChatTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
ensureMainThread(() -> {
|
||||
if (summarySheet == sheet && sheet.isShowing()) {
|
||||
sheet.failPendingResponse();
|
||||
}
|
||||
activeSummaryChatTask = null;
|
||||
if (!"Operation cancelled.".equals(error)) {
|
||||
showToastLong(str("revanced_gemini_error_api_failed", error));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (chatTask == null) {
|
||||
sheet.failPendingResponse();
|
||||
return;
|
||||
}
|
||||
activeSummaryChatTask = chatTask;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void cancelActiveSummaryChatTaskInternal() {
|
||||
boolean hadActiveTask = activeSummaryChatTask != null;
|
||||
if (hadActiveTask) {
|
||||
try {
|
||||
activeSummaryChatTask.cancel(true);
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error cancelling Gemini summary chat task", e);
|
||||
} finally {
|
||||
activeSummaryChatTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (hadActiveTask && summarySheet != null && summarySheet.isShowing()) {
|
||||
summarySheet.failPendingResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void dismissSummarySheetInternal() {
|
||||
cancelActiveSummaryChatTaskInternal();
|
||||
|
||||
if (summarySheet != null) {
|
||||
try {
|
||||
if (summarySheet.isShowing()) {
|
||||
summarySheet.dismiss();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error dismissing Gemini summary sheet", e);
|
||||
} finally {
|
||||
summarySheet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void dismissTranscriptionDialogInternal() {
|
||||
if (transcriptionDialogRef != null) {
|
||||
SheetBottomDialog.SlideDialog dialog = transcriptionDialogRef.get();
|
||||
if (dialog != null && dialog.isShowing()) {
|
||||
try {
|
||||
dialog.dismiss();
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error dismissing Gemini transcription sheet", e);
|
||||
}
|
||||
}
|
||||
transcriptionDialogRef.clear();
|
||||
transcriptionDialogRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Utility Methods
|
||||
|
||||
// region UI Methods: Dialogs
|
||||
|
|
@ -1362,97 +1556,55 @@ public final class GeminiManager {
|
|||
}
|
||||
|
||||
rebuildBaseLoadingMessage(opType);
|
||||
String initialMsg = (baseLoadingMessage != null && !baseLoadingMessage.isEmpty())
|
||||
? baseLoadingMessage
|
||||
: str("revanced_gemini_loading_default");
|
||||
|
||||
String timeSuffix = "";
|
||||
int elapsedSeconds = calculateElapsedTimeSeconds();
|
||||
if (elapsedSeconds >= 0) {
|
||||
timeSuffix = "\n" + elapsedSeconds + "s";
|
||||
}
|
||||
|
||||
String metaPrefix = "";
|
||||
if (currentVideoUrl != null) {
|
||||
String videoId = getVideoIdFromUrl(currentVideoUrl);
|
||||
String meta = videoMetadataCache.getOrDefault(videoId, "");
|
||||
if (!TextUtils.isEmpty(meta) && !meta.equals(str("revanced_gemini_loading_default"))) {
|
||||
metaPrefix = meta + "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
|
||||
if (opType == OperationType.SUMMARIZE) {
|
||||
builder.setTitle(str("revanced_gemini_summary_title"));
|
||||
} else {
|
||||
builder.setTitle(str("revanced_gemini_loading_default"));
|
||||
}
|
||||
|
||||
builder.setMessage(metaPrefix + initialMsg + timeSuffix);
|
||||
builder.setCancelable(false);
|
||||
|
||||
// Cancel Button
|
||||
builder.setNegativeButton(str("revanced_cancel"), (d, w) -> {
|
||||
final String urlAtClick = currentVideoUrl;
|
||||
|
||||
ensureMainThread(() -> {
|
||||
if (urlAtClick == null) {
|
||||
try {d.dismiss();} catch (Exception ignored) {}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Objects.equals(currentVideoUrl, urlAtClick)) {
|
||||
Logger.printInfo(() -> opType + " UI cancelled by user for " + urlAtClick);
|
||||
isCancelled = true;
|
||||
|
||||
String videoId = getVideoIdFromUrl(urlAtClick);
|
||||
String taskKey = getTaskKey(videoId, opType);
|
||||
|
||||
Future<?> f = activeTasks.remove(taskKey);
|
||||
|
||||
if (f != null) {
|
||||
try {
|
||||
f.cancel(true);
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error cancelling future for task: " + taskKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (opType == OperationType.TRANSCRIBE) {
|
||||
YandexVotUtils.forceReleaseWorkflowLock(urlAtClick);
|
||||
}
|
||||
|
||||
resetOperationStateInternal(opType, true);
|
||||
showToastShort(str("revanced_gemini_cancelled"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Minimize Button
|
||||
builder.setNeutralButton(str("revanced_minimize"), (d, w) -> {
|
||||
final String urlAtClick = currentVideoUrl;
|
||||
|
||||
ensureMainThread(() -> {
|
||||
assert urlAtClick != null;
|
||||
if (Objects.equals(currentVideoUrl, urlAtClick) && !isProgressDialogMinimized) {
|
||||
isProgressDialogMinimized = true;
|
||||
stopTimerInternal();
|
||||
dismissProgressDialogInternal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
final String statusText = (baseLoadingMessage != null && !baseLoadingMessage.isEmpty())
|
||||
? baseLoadingMessage
|
||||
: str("revanced_gemini_loading_default");
|
||||
final String videoUrlAtShow = currentVideoUrl;
|
||||
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
window.setLayout((int) (context.getResources().getDisplayMetrics().widthPixels * 0.90), WindowManager.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
loadingSheet = new GeminiBottomSheetUi.LoadingSheet(
|
||||
context,
|
||||
opType == OperationType.SUMMARIZE
|
||||
? str("revanced_gemini_summary_title")
|
||||
: str("revanced_gemini_loading_default"),
|
||||
getCurrentVideoMetaText(),
|
||||
statusText,
|
||||
() -> ensureMainThread(() -> {
|
||||
if (videoUrlAtShow == null) {
|
||||
return;
|
||||
}
|
||||
if (Objects.equals(currentVideoUrl, videoUrlAtShow)) {
|
||||
Logger.printInfo(() -> opType + " UI cancelled by user for " + videoUrlAtShow);
|
||||
isCancelled = true;
|
||||
|
||||
progressDialogRef = new WeakReference<>(dialog);
|
||||
String videoId = getVideoIdFromUrl(videoUrlAtShow);
|
||||
String taskKey = getTaskKey(videoId, opType);
|
||||
Future<?> future = activeTasks.remove(taskKey);
|
||||
if (future != null) {
|
||||
try {
|
||||
future.cancel(true);
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error cancelling future for task: " + taskKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (opType == OperationType.TRANSCRIBE) {
|
||||
YandexVotUtils.forceReleaseWorkflowLock(videoUrlAtShow);
|
||||
}
|
||||
|
||||
resetOperationStateInternal(opType, true);
|
||||
showToastShort(str("revanced_gemini_cancelled"));
|
||||
}
|
||||
}),
|
||||
() -> ensureMainThread(() -> {
|
||||
if (Objects.equals(currentVideoUrl, videoUrlAtShow) && !isProgressDialogMinimized) {
|
||||
isProgressDialogMinimized = true;
|
||||
stopTimerInternal();
|
||||
loadingSheet = null;
|
||||
}
|
||||
})
|
||||
);
|
||||
updateTimerMessageInternal();
|
||||
startTimerInternal();
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error showing progress dialog for " + opType, e);
|
||||
|
|
@ -1471,25 +1623,16 @@ public final class GeminiManager {
|
|||
@MainThread
|
||||
private void dismissProgressDialogInternal() {
|
||||
stopTimerInternal();
|
||||
|
||||
if (progressDialogRef != null) {
|
||||
AlertDialog dialog = progressDialogRef.get();
|
||||
if (dialog != null && dialog.isShowing()) {
|
||||
try {
|
||||
Context ctx = dialog.getContext();
|
||||
if (ctx instanceof Activity activity) {
|
||||
if (!activity.isFinishing() && !activity.isDestroyed()) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
} else {
|
||||
dialog.dismiss();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error dismissing progress dialog", e);
|
||||
if (loadingSheet != null) {
|
||||
try {
|
||||
if (loadingSheet.isShowing()) {
|
||||
loadingSheet.dismiss();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Error dismissing progress dialog", e);
|
||||
} finally {
|
||||
loadingSheet = null;
|
||||
}
|
||||
progressDialogRef.clear();
|
||||
progressDialogRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1505,71 +1648,43 @@ public final class GeminiManager {
|
|||
*/
|
||||
@MainThread
|
||||
private void showSummaryDialog(@NonNull Context context, @NonNull String summary, int seconds, @NonNull String videoUrl, @Nullable String usedModel) {
|
||||
StringBuilder summaryMetaInfo = new StringBuilder();
|
||||
|
||||
if (!TextUtils.isEmpty(usedModel)) summaryMetaInfo.append(usedModel).append('\n');
|
||||
if (seconds >= 0) summaryMetaInfo.append(str("revanced_gemini_time_taken", seconds));
|
||||
String timeMsg = summaryMetaInfo.length() > 0 ? "\n\n" + summaryMetaInfo : "";
|
||||
|
||||
// Prepare the header (Metadata)
|
||||
String videoId = getVideoIdFromUrl(videoUrl);
|
||||
String meta = videoMetadataCache.getOrDefault(videoId, "");
|
||||
String metaPrefix = "";
|
||||
if (!TextUtils.isEmpty(meta) && !meta.equals(str("revanced_gemini_loading_default"))) {
|
||||
metaPrefix = meta + "\n\n";
|
||||
}
|
||||
String normalizedSummary = normalizeGeminiText(summary);
|
||||
CharSequence formattedSummary = formatGeminiText(normalizedSummary, videoUrl);
|
||||
List<GeminiUtils.ChatMessage> cachedConversation = new ArrayList<>(
|
||||
Objects.requireNonNull(summaryConversationCache.getOrDefault(videoId, Collections.emptyList()))
|
||||
);
|
||||
|
||||
String normalizedSummary = summary.replace("\r\n", "\n").trim();
|
||||
dismissSummarySheetInternal();
|
||||
summarySheet = new GeminiBottomSheetUi.SummarySheet(
|
||||
context,
|
||||
getVideoMetaText(videoUrl),
|
||||
buildInfoText(seconds, usedModel),
|
||||
formattedSummary,
|
||||
normalizedSummary,
|
||||
cachedConversation,
|
||||
text -> formatGeminiText(text, videoUrl),
|
||||
text -> setClipboard(context, normalizeGeminiText(text), str("revanced_gemini_copy_success")),
|
||||
history -> summaryConversationCache.put(videoId, new ArrayList<>(history)),
|
||||
() -> ensureMainThread(() -> {
|
||||
cancelActiveSummaryChatTaskInternal();
|
||||
summarySheet = null;
|
||||
}),
|
||||
(sheet, question) -> ensureMainThread(() -> handleSummaryChatSend(videoUrl, normalizedSummary, sheet))
|
||||
);
|
||||
}
|
||||
|
||||
String displaySummary = wrapTimestampReferencesForPillStyle(normalizedSummary);
|
||||
Spanned formattedSummary = MarkdownUtils.fromMarkdown(displaySummary);
|
||||
SpannableStringBuilder clickableSummary = createClickableTimestampSummary(formattedSummary, videoUrl);
|
||||
@NonNull
|
||||
private String normalizeGeminiText(@NonNull String text) {
|
||||
return text.replace("\r\n", "\n").trim();
|
||||
}
|
||||
|
||||
SpannableStringBuilder summaryMessage = new SpannableStringBuilder();
|
||||
if (!metaPrefix.isEmpty()) {
|
||||
summaryMessage.append(metaPrefix);
|
||||
}
|
||||
summaryMessage.append(clickableSummary);
|
||||
summaryMessage.append(timeMsg);
|
||||
|
||||
TextView summaryTextView = new TextView(context);
|
||||
summaryTextView.setText(summaryMessage);
|
||||
summaryTextView.setTextColor(ThemeUtils.getAppForegroundColor());
|
||||
summaryTextView.setTextSize(16f);
|
||||
summaryTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
summaryTextView.setLinksClickable(true);
|
||||
|
||||
LinearLayout contentLayout = new LinearLayout(context);
|
||||
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
contentLayout.setPadding(dipToPixels(20), dipToPixels(10), dipToPixels(20), dipToPixels(8));
|
||||
contentLayout.addView(summaryTextView, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
ScrollView scrollView = new ScrollView(context);
|
||||
scrollView.setVerticalScrollBarEnabled(false);
|
||||
scrollView.addView(contentLayout, new ScrollView.LayoutParams(
|
||||
ScrollView.LayoutParams.MATCH_PARENT,
|
||||
ScrollView.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(str("revanced_gemini_summary_title"));
|
||||
builder.setView(scrollView);
|
||||
|
||||
AlertDialog dialog = builder
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
|
||||
.setNeutralButton(str("revanced_copy"), (d, w) -> setClipboard(context, normalizedSummary, str("revanced_gemini_copy_success")))
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
|
||||
dialog.show();
|
||||
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
window.setLayout((int) (context.getResources().getDisplayMetrics().widthPixels * 0.90), WindowManager.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
@NonNull
|
||||
private CharSequence formatGeminiText(@NonNull String text, @NonNull String videoUrl) {
|
||||
String normalizedText = normalizeGeminiText(text);
|
||||
String displayText = wrapTimestampReferencesForPillStyle(normalizedText);
|
||||
Spanned formattedText = MarkdownUtils.fromMarkdown(displayText);
|
||||
return createClickableTimestampText(formattedText, videoUrl);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -1600,14 +1715,14 @@ public final class GeminiManager {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
private SpannableStringBuilder createClickableTimestampSummary(@NonNull Spanned formattedSummary, @NonNull String videoUrl) {
|
||||
SpannableStringBuilder clickableSummary = new SpannableStringBuilder(formattedSummary);
|
||||
private SpannableStringBuilder createClickableTimestampText(@NonNull Spanned formattedText, @NonNull String videoUrl) {
|
||||
SpannableStringBuilder clickableText = new SpannableStringBuilder(formattedText);
|
||||
String videoId = getVideoIdFromUrl(videoUrl);
|
||||
if (TextUtils.isEmpty(videoId)) {
|
||||
return clickableSummary;
|
||||
return clickableText;
|
||||
}
|
||||
|
||||
Matcher matcher = SUMMARY_TIMESTAMP_PATTERN.matcher(clickableSummary.toString());
|
||||
Matcher matcher = SUMMARY_TIMESTAMP_PATTERN.matcher(clickableText.toString());
|
||||
while (matcher.find()) {
|
||||
final long timestampSeconds;
|
||||
try {
|
||||
|
|
@ -1617,14 +1732,14 @@ public final class GeminiManager {
|
|||
}
|
||||
|
||||
String vndLink = String.format(Locale.ENGLISH, VIDEO_SCHEME_INTENT_FORMAT, videoId, timestampSeconds);
|
||||
clickableSummary.setSpan(
|
||||
clickableText.setSpan(
|
||||
new TimestampClickableSpan(vndLink),
|
||||
matcher.start(),
|
||||
matcher.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
return clickableSummary;
|
||||
return clickableText;
|
||||
}
|
||||
|
||||
private long parseTimestampToSeconds(@NonNull Matcher matcher) {
|
||||
|
|
@ -1677,40 +1792,21 @@ public final class GeminiManager {
|
|||
@MainThread
|
||||
private void showTranscriptionResultDialogInternal(@NonNull Context context, @NonNull String rawTranscription, int seconds, @NonNull String videoUrl) {
|
||||
Logger.printDebug(() -> "Showing raw Gemini transcription dialog.");
|
||||
String timeMsg = (seconds >= 0) ? "\n\n" + str("revanced_gemini_time_taken", seconds) : "";
|
||||
|
||||
String metaPrefix = "";
|
||||
String videoId = getVideoIdFromUrl(videoUrl);
|
||||
String meta = videoMetadataCache.getOrDefault(videoId, "");
|
||||
if (!TextUtils.isEmpty(meta) && !meta.equals(str("revanced_gemini_loading_default"))) {
|
||||
metaPrefix = meta + "\n\n";
|
||||
}
|
||||
|
||||
String msg = metaPrefix + rawTranscription + timeMsg;
|
||||
|
||||
final String dialogVideoUrl = videoUrl;
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(str("revanced_gemini_transcription_result_title"));
|
||||
|
||||
AlertDialog dialog = builder.setMessage(msg)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
|
||||
.setNegativeButton(str("revanced_copy"), (d, w) -> setClipboard(context, rawTranscription, str("revanced_gemini_copy_success")))
|
||||
.setNeutralButton(str("revanced_gemini_transcription_parse_button"), (d, which) -> ensureMainThread(() -> {
|
||||
dismissTranscriptionDialogInternal();
|
||||
SheetBottomDialog.SlideDialog dialog = GeminiBottomSheetUi.showTranscriptionSheet(
|
||||
context,
|
||||
getVideoMetaText(videoUrl),
|
||||
buildInfoText(seconds, null),
|
||||
rawTranscription,
|
||||
() -> setClipboard(context, rawTranscription, str("revanced_gemini_copy_success")),
|
||||
() -> ensureMainThread(() -> {
|
||||
Logger.printDebug(() -> "Parse button clicked (Gemini raw). Attempting parse and display overlay.");
|
||||
hideTranscriptionOverlayInternal();
|
||||
parseAndShowTranscriptionInternal(rawTranscription, dialogVideoUrl);
|
||||
try { d.dismiss(); } catch (Exception ignored) {}
|
||||
}))
|
||||
.create();
|
||||
|
||||
dialog.show();
|
||||
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
window.setLayout((int) (context.getResources().getDisplayMetrics().widthPixels * 0.90), WindowManager.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
})
|
||||
);
|
||||
transcriptionDialogRef = new WeakReference<>(dialog);
|
||||
}
|
||||
|
||||
// endregion UI Methods: Dialogs
|
||||
|
|
@ -2009,13 +2105,11 @@ public final class GeminiManager {
|
|||
*/
|
||||
@MainThread
|
||||
private void startTimerInternal() {
|
||||
AlertDialog currentDialog = (progressDialogRef != null) ? progressDialogRef.get() : null;
|
||||
|
||||
if (currentOperation == OperationType.NONE
|
||||
|| isCancelled
|
||||
|| isProgressDialogMinimized
|
||||
|| currentDialog == null
|
||||
|| !currentDialog.isShowing()) {
|
||||
|| loadingSheet == null
|
||||
|| !loadingSheet.isShowing()) {
|
||||
stopTimerInternal();
|
||||
return;
|
||||
}
|
||||
|
|
@ -2025,8 +2119,11 @@ public final class GeminiManager {
|
|||
timerRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AlertDialog currentDialog = progressDialogRef.get();
|
||||
if (currentOperation != OperationType.NONE && !isCancelled && !isProgressDialogMinimized && currentDialog != null && currentDialog.isShowing()) {
|
||||
if (currentOperation != OperationType.NONE
|
||||
&& !isCancelled
|
||||
&& !isProgressDialogMinimized
|
||||
&& loadingSheet != null
|
||||
&& loadingSheet.isShowing()) {
|
||||
updateTimerMessageInternal();
|
||||
timerHandler.postDelayed(this, 1000);
|
||||
} else {
|
||||
|
|
@ -2057,40 +2154,24 @@ public final class GeminiManager {
|
|||
*/
|
||||
@MainThread
|
||||
private void updateTimerMessageInternal() {
|
||||
if (progressDialogRef == null) {
|
||||
if (loadingSheet == null) {
|
||||
stopTimerInternal();
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog currentDialog = progressDialogRef.get();
|
||||
|
||||
if (currentDialog != null && currentDialog.isShowing() && !isCancelled && !isProgressDialogMinimized) {
|
||||
if (loadingSheet.isShowing() && !isCancelled && !isProgressDialogMinimized) {
|
||||
int sec = calculateElapsedTimeSeconds();
|
||||
if (sec < 0) return;
|
||||
if (sec < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String time = "\n" + sec + "s";
|
||||
String base = (baseLoadingMessage != null && !baseLoadingMessage.isEmpty())
|
||||
? baseLoadingMessage
|
||||
: str("revanced_gemini_loading_default");
|
||||
|
||||
String metaPrefix = "";
|
||||
if (currentVideoUrl != null) {
|
||||
String videoId = getVideoIdFromUrl(currentVideoUrl);
|
||||
String meta = videoMetadataCache.getOrDefault(videoId, "");
|
||||
if (!TextUtils.isEmpty(meta) && !meta.equals(str("revanced_gemini_loading_default"))) {
|
||||
metaPrefix = meta + "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
String msg = metaPrefix + base + time;
|
||||
|
||||
try {
|
||||
TextView tv = currentDialog.findViewById(android.R.id.message);
|
||||
if (tv != null) {
|
||||
tv.setText(msg);
|
||||
} else {
|
||||
currentDialog.setMessage(msg);
|
||||
}
|
||||
loadingSheet.updateMeta(getCurrentVideoMetaText());
|
||||
loadingSheet.updateStatus(base);
|
||||
loadingSheet.updateElapsed(sec);
|
||||
} catch (Exception e) {
|
||||
stopTimerInternal();
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -170,8 +170,10 @@ val overlayButtonsPatch = resourcePatch(
|
|||
"drawable",
|
||||
"playlist_repeat_button.xml",
|
||||
"playlist_shuffle_button.xml",
|
||||
"revanced_repeat_button.xml",
|
||||
"revanced_gemini_copy.xml",
|
||||
"revanced_gemini_send.xml",
|
||||
"revanced_mute_volume_button.xml",
|
||||
"revanced_repeat_button.xml",
|
||||
"revanced_vot_button.xml",
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M 14.668 12.667 C 14.668 11.957 14.668 11.463 14.637 11.079 C 14.626 10.87 14.588 10.663 14.524 10.464 L 14.469 10.334 C 14.315 10.033 14.081 9.78 13.793 9.603 L 13.666 9.531 C 13.508 9.451 13.296 9.394 12.921 9.363 C 12.537 9.332 12.044 9.332 11.333 9.332 L 8.5 9.332 C 7.789 9.332 7.296 9.332 6.912 9.363 C 6.703 9.373 6.496 9.411 6.297 9.476 L 6.167 9.531 C 5.865 9.685 5.613 9.919 5.436 10.207 L 5.366 10.334 C 5.285 10.492 5.228 10.704 5.197 11.079 C 5.166 11.463 5.165 11.956 5.165 12.667 L 5.165 15.5 C 5.165 16.211 5.165 16.704 5.197 17.088 C 5.228 17.464 5.285 17.675 5.365 17.833 L 5.435 17.959 C 5.612 18.247 5.865 18.481 6.167 18.635 L 6.297 18.691 C 6.441 18.743 6.63 18.78 6.912 18.803 C 7.296 18.834 7.789 18.835 8.5 18.835 L 11.333 18.835 C 12.043 18.835 12.537 18.835 12.921 18.803 C 13.297 18.772 13.508 18.715 13.666 18.635 L 13.793 18.565 C 14.08 18.388 14.315 18.135 14.469 17.833 L 14.524 17.703 C 14.576 17.559 14.614 17.37 14.637 17.088 C 14.668 16.704 14.668 16.211 14.668 15.5 L 14.668 12.667 Z M 15.998 14.665 C 16.453 14.663 16.801 14.66 17.088 14.637 C 17.464 14.606 17.675 14.549 17.833 14.469 L 17.959 14.398 C 18.247 14.221 18.481 13.968 18.635 13.666 L 18.691 13.536 C 18.755 13.337 18.792 13.13 18.803 12.921 C 18.834 12.537 18.835 12.044 18.835 11.333 L 18.835 8.5 C 18.835 7.789 18.835 7.296 18.803 6.912 C 18.792 6.703 18.755 6.496 18.691 6.297 L 18.635 6.167 C 18.481 5.866 18.247 5.613 17.959 5.436 L 17.833 5.366 C 17.675 5.285 17.463 5.228 17.088 5.197 C 16.704 5.166 16.211 5.165 15.5 5.165 L 12.667 5.165 C 11.957 5.165 11.463 5.166 11.079 5.197 C 10.797 5.22 10.608 5.257 10.464 5.309 L 10.334 5.365 C 10.032 5.519 9.78 5.753 9.603 6.041 L 9.531 6.167 C 9.451 6.325 9.394 6.537 9.363 6.912 C 9.34 7.199 9.336 7.547 9.334 8.002 L 11.333 8.002 C 12.022 8.002 12.579 8.002 13.029 8.038 C 13.487 8.076 13.894 8.155 14.271 8.347 L 14.488 8.469 C 14.984 8.773 15.388 9.209 15.653 9.729 L 15.72 9.872 C 15.864 10.209 15.93 10.57 15.962 10.971 C 15.999 11.421 15.998 11.978 15.998 12.667 L 15.998 14.665 Z M 20.165 11.333 C 20.165 12.022 20.165 12.579 20.129 13.029 C 20.096 13.43 20.031 13.791 19.887 14.128 L 19.82 14.271 C 19.555 14.791 19.15 15.227 18.655 15.531 L 18.436 15.653 C 18.06 15.845 17.654 15.924 17.196 15.962 C 16.859 15.989 16.462 15.993 15.996 15.995 C 15.993 16.462 15.989 16.859 15.962 17.196 C 15.929 17.597 15.864 17.958 15.72 18.294 L 15.653 18.436 C 15.388 18.958 14.984 19.394 14.488 19.698 L 14.271 19.82 C 13.894 20.012 13.487 20.091 13.029 20.129 C 12.579 20.166 12.022 20.165 11.333 20.165 L 8.5 20.165 C 7.81 20.165 7.254 20.165 6.804 20.129 C 6.404 20.096 6.042 20.031 5.706 19.887 L 5.563 19.82 C 5.043 19.555 4.607 19.152 4.302 18.655 L 4.18 18.436 C 3.988 18.06 3.909 17.654 3.871 17.196 C 3.834 16.746 3.835 16.189 3.835 15.5 L 3.835 12.667 C 3.835 11.978 3.835 11.421 3.871 10.971 C 3.909 10.513 3.988 10.106 4.18 9.729 L 4.302 9.512 C 4.606 9.016 5.042 8.612 5.563 8.347 L 5.706 8.28 C 6.042 8.136 6.403 8.07 6.804 8.038 C 7.141 8.011 7.537 8.006 8.004 8.004 C 8.006 7.537 8.011 7.141 8.038 6.804 C 8.075 6.346 8.155 5.94 8.347 5.564 L 8.469 5.344 C 8.773 4.849 9.209 4.445 9.729 4.18 L 9.872 4.113 C 10.209 3.969 10.57 3.903 10.971 3.871 C 11.421 3.834 11.978 3.835 12.667 3.835 L 15.5 3.835 C 16.19 3.835 16.746 3.835 17.196 3.871 C 17.654 3.909 18.06 3.988 18.436 4.18 L 18.656 4.302 C 19.151 4.606 19.555 5.042 19.82 5.563 L 19.887 5.706 C 20.031 6.042 20.097 6.403 20.129 6.804 C 20.166 7.254 20.165 7.811 20.165 8.5 L 20.165 11.333 Z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M 10.812 19.185 L 10.812 7.704 L 6.868 11.648 C 6.567 11.96 6.123 12.085 5.704 11.975 C 5.284 11.866 4.957 11.538 4.848 11.12 C 4.738 10.701 4.863 10.255 5.174 9.955 L 11.162 3.966 L 11.254 3.885 C 11.73 3.498 12.422 3.533 12.856 3.966 L 18.844 9.955 L 18.926 10.046 C 19.31 10.522 19.272 11.211 18.84 11.643 C 18.407 12.076 17.719 12.113 17.242 11.73 L 17.151 11.648 L 13.207 7.704 L 13.207 19.185 C 13.207 19.847 12.672 20.383 12.009 20.383 C 11.348 20.383 10.812 19.847 10.812 19.185"/>
|
||||
</vector>
|
||||
|
|
@ -636,6 +636,7 @@ Known issue: Sometimes the swipe overlay appears before switching to fullscreen
|
|||
<string name="revanced_gemini_loading_summarize">Summarizing the video...</string>
|
||||
<string name="revanced_gemini_loading_transcribe">Transcribing the video...</string>
|
||||
<string name="revanced_gemini_loading_yandex_transcribe">Yandex: Getting subtitles...</string>
|
||||
<string name="revanced_gemini_chat_input_hint">Ask about this video</string>
|
||||
<string name="revanced_gemini_status_translating">Translating with Gemini...\n\n⚠️ This step may fail if Gemini ignores the instructions or hits the limit.</string>
|
||||
<string name="revanced_gemini_summary_title">Gemini Video Summary</string>
|
||||
<string name="revanced_gemini_time_taken">(Took %d seconds)</string>
|
||||
|
|
@ -645,6 +646,7 @@ Known issue: Sometimes the swipe overlay appears before switching to fullscreen
|
|||
<string name="revanced_gemini_transcription_parse_button">Subtitles</string>
|
||||
<string name="revanced_gemini_transcription_parse_success">Subtitle parsed successfully</string>
|
||||
<string name="revanced_gemini_transcription_result_title">Gemini Video Transcription</string>
|
||||
<string name="revanced_send">Send</string>
|
||||
<string name="revanced_gms_show_dialog_summary">Displays the optimization dialog for GMSCore at each application startup.</string>
|
||||
<string name="revanced_gms_show_dialog_title">Show optimization dialog for GMSCore</string>
|
||||
<string name="revanced_gradient_seekbar_positions_summary">Gradient start and end positions from 0 to 1 separated by comma.</string>
|
||||
|
|
@ -1753,8 +1755,8 @@ AI-generated responses may contain inaccurate information.
|
|||
Tap here to get an API key."</string>
|
||||
<string name="revanced_overlay_button_gemini_about_title">About Gemini</string>
|
||||
<string name="revanced_overlay_button_gemini_summarize">Summarize</string>
|
||||
<string name="revanced_overlay_button_gemini_summarize_api_key_summary">"Get API key at https://aistudio.google.com/apikey"</string>
|
||||
<string name="revanced_overlay_button_gemini_summarize_api_key_title">Gemini API key</string>
|
||||
<string name="revanced_overlay_button_gemini_summarize_api_key_summary">"Enter one Gemini API key per line. Get API keys at https://aistudio.google.com/apikey"</string>
|
||||
<string name="revanced_overlay_button_gemini_summarize_api_key_title">Gemini API keys</string>
|
||||
<string name="revanced_overlay_button_gemini_summarize_summary">"Tap to summarize the video. Tap and hold to transcribe.
|
||||
|
||||
Summarized and transcribed results are saved unless you closed the app or used Gemini on another video. Tap the button again to view results."</string>
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@
|
|||
<!-- SETTINGS: OVERLAY_BUTTONS
|
||||
<PreferenceCategory android:title="@string/revanced_preference_category_overlay_buttons" android:layout="@layout/revanced_settings_preferences_category" />
|
||||
<SwitchPreference android:title="@string/revanced_overlay_button_gemini_summarize_title" android:key="revanced_overlay_button_gemini_summarize" android:summary="@string/revanced_overlay_button_gemini_summarize_summary" />
|
||||
<app.morphe.extension.shared.settings.preference.ResettableEditTextPreference android:hint="AIzaSy..." android:title="@string/revanced_overlay_button_gemini_summarize_api_key_title" android:key="revanced_overlay_button_gemini_summarize_api_key" android:summary="@string/revanced_overlay_button_gemini_summarize_api_key_summary" android:inputType="text" />
|
||||
<app.morphe.extension.shared.settings.preference.ResettableEditTextPreference android:hint="AIzaSy..." android:title="@string/revanced_overlay_button_gemini_summarize_api_key_title" android:key="revanced_overlay_button_gemini_summarize_api_key" android:summary="@string/revanced_overlay_button_gemini_summarize_api_key_summary" android:inputType="textMultiLine" />
|
||||
<app.morphe.extension.shared.settings.preference.ResettableEditTextPreference android:hint="14" android:title="@string/revanced_gemini_transcribe_subtitles_font_size_title" android:key="revanced_gemini_transcribe_subtitles_font_size" android:summary="@string/revanced_gemini_transcribe_subtitles_font_size_summary" android:inputType="number" />
|
||||
<SwitchPreference android:title="@string/revanced_yandex_transcribe_subtitles_title" android:key="revanced_yandex_transcribe_subtitles" android:summary="@string/revanced_yandex_transcribe_subtitles_summary" />
|
||||
<app.morphe.extension.shared.settings.preference.ResettableEditTextPreference android:hint="en" android:title="@string/revanced_yandex_transcribe_subtitles_language_title" android:key="revanced_yandex_transcribe_subtitles_language" android:summary="@string/revanced_yandex_transcribe_subtitles_language_summary" android:inputType="text" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue