mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-30 21:55:10 +00:00
Let user open in the PC browser media URL to Android device [1/2]
Seek position is preserved for following domains: youtube.com youtu.be pornhub.com vimeo.com dailymotion.com twitch.tv ic_arrow_black.xml was converted from Kubuntu 19.10 AMD64 breeze-icon-theme package: /usr/share/icons/breeze-dark/actions/32/arrow.svg
This commit is contained in:
9
res/drawable/ic_arrow_black.xml
Normal file
9
res/drawable/ic_arrow_black.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32"
|
||||
android:width="32dp"
|
||||
android:height="32dp">
|
||||
<path
|
||||
android:pathData="M8.990234 5c-0.5522847 0 -1 0.4477153 -1 1 0 0.5522847 0.4477153 1 1 1 0.5522847 0 1 -0.4477153 1 -1 0 -0.5522847 -0.4477153 -1 -1 -1zm2 4L11 29 16.841796 21.769531 26 21Z"
|
||||
android:fillColor="#FF000000" />
|
||||
</vector>
|
@@ -101,6 +101,14 @@
|
||||
android:src="@drawable/ic_next_black"
|
||||
android:theme="@style/DisableableButton" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/open_url_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="0.25"
|
||||
android:contentDescription="@string/mpris_open_url"
|
||||
android:src="@drawable/ic_arrow_black"
|
||||
android:theme="@style/DisableableButton" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -289,4 +289,7 @@
|
||||
<string name="no_app_for_opening">Для этого файла нет приложений, способных его открыть</string>
|
||||
<string name="remote_keyboard_service">Удалённая клавиатура KDE Connect</string>
|
||||
<string name="presenter_pointer">Указатель</string>
|
||||
<string name="mpris_open_url">Открыть URL</string>
|
||||
<string name="cant_open_url">Не удалось открыть URL</string>
|
||||
<string name="cant_format_seek_uri">Не удалось задать позицию в URL</string>
|
||||
</resources>
|
||||
|
@@ -354,4 +354,7 @@
|
||||
<string name="location_permission_needed_title">Permission required</string>
|
||||
<string name="location_permission_needed_desc">Android requires the Location permission to identify your WiFi network</string>
|
||||
<string name="clipboard_android_x_incompat">Android 10 has removed clipboard access to all apps. This plugin will be disabled.</string>
|
||||
<string name="mpris_open_url">Open URL</string>
|
||||
<string name="cant_open_url">Can\'t open URL</string>
|
||||
<string name="cant_format_seek_uri">Can\'t format URL for seek</string>
|
||||
</resources>
|
||||
|
145
src/org/kde/kdeconnect/Helpers/VideoUrlsHelper.java
Normal file
145
src/org/kde/kdeconnect/Helpers/VideoUrlsHelper.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package org.kde.kdeconnect.Helpers;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
public class VideoUrlsHelper {
|
||||
private static final int SECONDS_IN_MINUTE = 60;
|
||||
private static final int MINUTES_IN_HOUR = 60;
|
||||
private static final int SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
|
||||
|
||||
public static URL formatUriWithSeek(String address, long position)
|
||||
throws MalformedURLException {
|
||||
URL url = new URL(address);
|
||||
position /= 1000; // Convert ms to seconds
|
||||
if (position <= 0) {
|
||||
return url; // nothing to do
|
||||
}
|
||||
String host = url.getHost().toLowerCase();
|
||||
|
||||
// Most common settings as defaults:
|
||||
String parameter = "t="; // Characters before timestamp
|
||||
String timestamp = Long.toString(position); // Timestamp itself
|
||||
String trailer = ""; // Characters after timestamp
|
||||
// true - search/add to query URL part (between ? and # signs),
|
||||
// false - search/add timestamp to ref (anchor) URL part (after # sign),
|
||||
boolean inQuery = true;
|
||||
// true - We know how to format URL with seek timestamp, false - not
|
||||
boolean seekUrl = false;
|
||||
|
||||
// Override defaults if necessary
|
||||
if (host.contains("youtube.com")
|
||||
|| host.contains("youtu.be")
|
||||
|| host.contains("pornhub.com")) {
|
||||
seekUrl = true;
|
||||
url = stripTimestampS(url, parameter, trailer, inQuery);
|
||||
} else if (host.contains("vimeo.com")) {
|
||||
seekUrl = true;
|
||||
trailer = "s";
|
||||
url = stripTimestampS(url, parameter, trailer, inQuery);
|
||||
} else if (host.contains("dailymotion.com")) {
|
||||
seekUrl = true;
|
||||
parameter = "start=";
|
||||
url = stripTimestampS(url, parameter, trailer, inQuery);
|
||||
} else if (host.contains("twitch.tv")) {
|
||||
seekUrl = true;
|
||||
timestamp = formatTimestampHMS(position, true);
|
||||
url = stripTimestampHMS(url, parameter, trailer, inQuery);
|
||||
}
|
||||
|
||||
if (seekUrl) {
|
||||
url = formatUrlWithSeek(url, timestamp, parameter, trailer, inQuery);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Returns timestamp in 1h2m34s or 01h02m34s (according to padWithZeroes)
|
||||
private static String formatTimestampHMS(long seconds, boolean padWithZeroes) {
|
||||
if (seconds == 0) {
|
||||
return "0s";
|
||||
}
|
||||
|
||||
int sec = (int) (seconds % SECONDS_IN_MINUTE);
|
||||
int min = (int) ((seconds / SECONDS_IN_MINUTE) % MINUTES_IN_HOUR);
|
||||
int hour = (int) (seconds / SECONDS_IN_HOUR);
|
||||
|
||||
String hours = hour > 0 ? hour + "h" : "";
|
||||
String mins = min > 0 || hour > 0 ? min + "m" : "";
|
||||
String secs = sec + "s";
|
||||
|
||||
String value;
|
||||
if (padWithZeroes) {
|
||||
String hoursPad = hour > 9 ? "" : "0";
|
||||
String minsPad = min > 9 ? "" : "0";
|
||||
String secsPad = sec > 9 ? "" : "0";
|
||||
value = hoursPad + hours + minsPad + mins + secsPad + secs;
|
||||
} else {
|
||||
value = hours + mins + secs;
|
||||
}
|
||||
return value;
|
||||
|
||||
}
|
||||
|
||||
// Remove timestamp in 01h02m34s or 1h2m34s or 02m34s or 2m34s or 01s or 1s format.
|
||||
// Can also nandle rimestamps in 1234s format if called with 's' trailer
|
||||
private static URL stripTimestampHMS(URL url, String parameter, String trailer, boolean inQuery)
|
||||
throws MalformedURLException {
|
||||
String regex = parameter + "([\\d]+[hH])?([\\d]+[mM])?[\\d]+[sS]" + trailer + "&?";
|
||||
return stripTimestampCommon(url, inQuery, regex);
|
||||
}
|
||||
|
||||
|
||||
// Remove timestamp in 1234 format
|
||||
private static URL stripTimestampS(URL url, String parameter, String trailer, boolean inQuery)
|
||||
throws MalformedURLException {
|
||||
String regex = parameter + "[\\d]+" + trailer + "&?";
|
||||
return stripTimestampCommon(url, inQuery, regex);
|
||||
}
|
||||
|
||||
private static URL stripTimestampCommon(URL url, boolean inQuery, String regex)
|
||||
throws MalformedURLException {
|
||||
String value;
|
||||
if (inQuery) {
|
||||
value = url.getQuery();
|
||||
} else {
|
||||
value = url.getRef();
|
||||
}
|
||||
if (value == null) {
|
||||
return url;
|
||||
}
|
||||
String newValue = value.replaceAll(regex, "");
|
||||
String replaced = url.toString().replaceFirst(value, newValue);
|
||||
if (inQuery && replaced.endsWith("&")) {
|
||||
replaced = replaced.substring(0, replaced.length() - 1);
|
||||
}
|
||||
return new URL(replaced);
|
||||
}
|
||||
|
||||
private static URL formatUrlWithSeek(URL url, String position, String parameter, String trailer,
|
||||
boolean inQuery) throws MalformedURLException {
|
||||
String value;
|
||||
String separator;
|
||||
String newValue;
|
||||
if (inQuery) {
|
||||
value = url.getQuery();
|
||||
separator = "?";
|
||||
} else {
|
||||
value = url.getRef();
|
||||
separator = "#";
|
||||
}
|
||||
if (value == null) {
|
||||
newValue = String.format(Locale.getDefault(), "%s%s%s%s%s",
|
||||
url.toString(), separator, parameter, position, trailer);
|
||||
return new URL(newValue);
|
||||
}
|
||||
if (inQuery) {
|
||||
newValue = String.format(Locale.getDefault(), "%s&%s%s%s",
|
||||
value, parameter, position, trailer);
|
||||
} else {
|
||||
newValue = String.format(Locale.getDefault(), "%s%s%s",
|
||||
parameter, position, trailer);
|
||||
}
|
||||
return new URL(url.toString().replaceFirst(value, newValue));
|
||||
}
|
||||
}
|
@@ -20,9 +20,12 @@
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
@@ -37,15 +40,18 @@ import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.kde.kdeconnect.Backends.BaseLink;
|
||||
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Helpers.VideoUrlsHelper;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemvolumeFragment;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -77,6 +83,9 @@ public class MprisActivity extends AppCompatActivity {
|
||||
@BindView(R.id.ff_button)
|
||||
ImageButton ffButton;
|
||||
|
||||
@BindView(R.id.open_url_button)
|
||||
ImageButton openUrlButton;
|
||||
|
||||
@BindView(R.id.time_textview)
|
||||
TextView timeText;
|
||||
|
||||
@@ -288,6 +297,7 @@ public class MprisActivity extends AppCompatActivity {
|
||||
volumeLayout.setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.GONE);
|
||||
rewButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||
ffButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE);
|
||||
openUrlButton.setVisibility(playerStatus.getUrl() != null ? View.VISIBLE : View.GONE);
|
||||
|
||||
//Show and hide previous/next buttons simultaneously
|
||||
if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) {
|
||||
@@ -387,6 +397,23 @@ public class MprisActivity extends AppCompatActivity {
|
||||
|
||||
performActionOnClick(ffButton, p -> p.seek(interval_time));
|
||||
|
||||
performActionOnClick(openUrlButton, p -> {
|
||||
String url = p.getUrl();
|
||||
try {
|
||||
url = VideoUrlsHelper.formatUriWithSeek(p.getUrl(), p.getPosition()).toString();
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getApplicationContext(), String.format("%s '%s'", getString(R.string.cant_format_seek_uri), p.getUrl()), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
try {
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getApplicationContext(), String.format("%s '%s'", getString(R.string.cant_open_url), p.getUrl()), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
performActionOnClick(nextButton, MprisPlugin.MprisPlayer::next);
|
||||
|
||||
performActionOnClick(stopButton, MprisPlugin.MprisPlayer::stop);
|
||||
|
@@ -20,6 +20,7 @@
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MprisPlugin;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -53,6 +54,7 @@ public class MprisPlugin extends Plugin {
|
||||
private String artist = "";
|
||||
private String album = "";
|
||||
private String albumArtUrl = "";
|
||||
private String url = "";
|
||||
private int volume = 50;
|
||||
private long length = -1;
|
||||
private long lastPosition = 0;
|
||||
@@ -136,6 +138,11 @@ public class MprisPlugin extends Plugin {
|
||||
return AlbumArtCache.getAlbumArt(albumArtUrl, MprisPlugin.this, player);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public boolean isSetVolumeAllowed() {
|
||||
return !isSpotify();
|
||||
}
|
||||
@@ -282,6 +289,7 @@ public class MprisPlugin extends Plugin {
|
||||
playerStatus.title = np.getString("title", playerStatus.title);
|
||||
playerStatus.artist = np.getString("artist", playerStatus.artist);
|
||||
playerStatus.album = np.getString("album", playerStatus.album);
|
||||
playerStatus.url = np.getString("url", playerStatus.url);
|
||||
playerStatus.volume = np.getInt("volume", playerStatus.volume);
|
||||
playerStatus.length = np.getLong("length", playerStatus.length);
|
||||
if (np.has("pos")) {
|
||||
|
Reference in New Issue
Block a user