replaced VideoView with ExoPlayer, removed Twitter linking, removed unused classes and resources, version upgrade, bug fix

This commit is contained in:
nuclearfog 2023-04-10 01:38:51 +02:00
parent 8bfd045f0a
commit 1cdf1b7d0a
No known key found for this signature in database
GPG Key ID: 03488A185C476379
35 changed files with 144 additions and 1019 deletions

View File

@ -1,6 +1,5 @@
plugins {
id 'com.android.application'
id 'io.michaelrocks.paranoid'
id 'ru.cleverpumpkin.proguard-dictionaries-generator'
}
@ -13,8 +12,8 @@ android {
applicationId 'org.nuclearfog.twidda'
minSdkVersion 21
targetSdkVersion 33
versionCode 79
versionName '3.0.11'
versionCode 80
versionName '3.1'
resConfigs 'en', 'de-rDE', 'zh-rCN'
}
@ -40,10 +39,6 @@ android {
excludes += ['/META-INF/CHANGES', '/META-INF/DEPENDENCIES', '/META-INF/README.md', '/META-INF/androidx.*', '/META-INF/kotlin*', '/META-INF/com.*', '/META-INF/services/**', '/META-INF/com/**', '/kotlin/**', '/Debug*']
}
}
paranoid {
enabled true
}
}
proguardDictionaries {
@ -54,8 +49,11 @@ proguardDictionaries {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.5'
implementation 'com.google.android.exoplayer:exoplayer-core:2.18.5'
implementation 'com.google.android.exoplayer:extension-okhttp:2.18.5'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.cardview:cardview:1.0.0'

View File

@ -40,24 +40,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- DEPRECATED
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="twitter.com" />
<data
android:scheme="https"
android:host="www.twitter.com" />
<data
android:scheme="https"
android:host="mobile.twitter.com" />
</intent-filter>
-->
</activity>
<activity
@ -102,7 +84,6 @@
<activity
android:name=".ui.activities.VideoViewer"
android:screenOrientation="portrait"
android:theme="@style/Transparency" />
<activity

View File

@ -181,29 +181,6 @@
limitations under the License.
</pre>
<h3>Notices for libraries:</h3>
<ul>
<li>paranoid</li>
</ul>
<pre>
Copyright 2021 Michael Rozumyanskiy
Licensed under the Apache License,
Version 2.0 (the "License");
you may not use this file except
in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law
or agreed to in writing,
software distributed under the License
is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied.
See the License for the specific
language governing permissions and
limitations under the License.
</pre>
</body>
</html>

View File

@ -5,14 +5,11 @@ import android.content.Context;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.Account;
import io.michaelrocks.paranoid.Obfuscate;
/**
* this class manages Twitter oauth 1.0 keys (consumer token & token secret) for API V1.1 & V2
*
* @author nuclearfog
*/
@Obfuscate
public class Tokens {
/**

View File

@ -40,6 +40,7 @@ public class MediaV1 implements Media {
private int type = NONE;
private String url = "";
private String previewUrl = "";
private String key;
/**
@ -53,6 +54,7 @@ public class MediaV1 implements Media {
String url = json.getString("media_url_https");
if (Patterns.WEB_URL.matcher(url).matches()) {
this.url = url;
previewUrl = url;
} else {
throw new JSONException("invalid url: \"" + url + "\"");
}
@ -62,6 +64,7 @@ public class MediaV1 implements Media {
case TYPE_VIDEO:
int maxBitrate = -1;
JSONArray videoVariants = json.getJSONObject("video_info").getJSONArray("variants");
previewUrl = json.optString("media_url_https", "");
for (int i = 0; i < videoVariants.length(); i++) {
JSONObject variant = videoVariants.getJSONObject(i);
int bitRate = variant.optInt("bitrate", 0);
@ -80,6 +83,7 @@ public class MediaV1 implements Media {
case TYPE_GIF:
JSONArray gifVariants = json.getJSONObject("video_info").getJSONArray("variants");
previewUrl = json.optString("media_url_https", "");
for (int i = 0; i < gifVariants.length(); i++) {
JSONObject gifVariant = gifVariants.getJSONObject(i);
if (MIME_V_MP4.equals(gifVariant.getString("content_type"))) {
@ -118,7 +122,7 @@ public class MediaV1 implements Media {
@Override
public String getPreviewUrl() {
return url;
return previewUrl;
}

View File

@ -1,181 +0,0 @@
package org.nuclearfog.twidda.backend.async;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.nuclearfog.twidda.backend.api.Connection;
import org.nuclearfog.twidda.backend.api.ConnectionException;
import org.nuclearfog.twidda.backend.api.ConnectionManager;
import org.nuclearfog.twidda.model.User;
import org.nuclearfog.twidda.model.UserList;
import org.nuclearfog.twidda.ui.activities.MainActivity;
import org.nuclearfog.twidda.ui.activities.ProfileActivity;
import org.nuclearfog.twidda.ui.activities.SearchActivity;
import org.nuclearfog.twidda.ui.activities.StatusActivity;
import org.nuclearfog.twidda.ui.activities.StatusEditor;
import org.nuclearfog.twidda.ui.activities.UserlistActivity;
import org.nuclearfog.twidda.ui.activities.UserlistsActivity;
import java.util.List;
/**
* This class handles deep links and starts activities to show the content
* When the user clicks on a link (e.g. "twitter.com/Twitter/status/1480571976414543875")
* this class extracts information of the link and open an activity tp show the content
* When a link type isn't supported, the {@link MainActivity} will be opened instead
*
* @author nuclearfog
* @see MainActivity
*/
public class LinkLoader extends AsyncExecutor<Uri, LinkLoader.LinkResult> {
private Connection connection;
/**
*
*/
public LinkLoader(Context context) {
connection = ConnectionManager.getDefaultConnection(context);
}
@Override
protected LinkResult doInBackground(@NonNull Uri link) {
try {
List<String> pathSeg = link.getPathSegments();
Bundle data = new Bundle();
if (!pathSeg.isEmpty()) {
// open home timeline tab
// e.g. twitter.com/home
if (pathSeg.get(0).equals("home")) {
data.putInt(MainActivity.KEY_TAB_PAGE, 0);
return new LinkResult(data, MainActivity.class);
}
// open trend tab
// e.g. twitter.com/trends , twitter.com/explore or twitter.com/i/trends
else if (pathSeg.get(0).equals("trends") || pathSeg.get(0).equals("explore") ||
(pathSeg.size() == 2 && pathSeg.get(0).equals("i") && pathSeg.get(1).equals("trends"))) {
data.putInt(MainActivity.KEY_TAB_PAGE, 1);
return new LinkResult(data, MainActivity.class);
}
// open mentions timeline
// e.g. twitter.com/notifications
else if (pathSeg.get(0).equals("notifications")) {
data.putInt(MainActivity.KEY_TAB_PAGE, 2);
return new LinkResult(data, MainActivity.class);
}
// open directmessage page
// e.g. twitter.com/messages
else if (pathSeg.get(0).equals("messages")) {
data.putInt(MainActivity.KEY_TAB_PAGE, 3);
return new LinkResult(data, MainActivity.class);
}
// open twitter search
// e.g. twitter.com/search?q={search string}
else if (pathSeg.get(0).equals("search")) {
if (link.isHierarchical()) {
String search = link.getQueryParameter("q");
if (search != null) {
data.putString(SearchActivity.KEY_SEARCH_QUERY, search);
return new LinkResult(data, SearchActivity.class);
}
}
}
// open status editor and add text
// e.g. twitter.com/share or twitter.com/intent/status
else if (pathSeg.get(0).equals("share") ||
(pathSeg.size() == 2 && pathSeg.get(0).equals("intent") && pathSeg.get(1).equals("tweet"))) {
if (link.isHierarchical()) {
String status = "";
String text = link.getQueryParameter("text");
String url = link.getQueryParameter("url");
String via = link.getQueryParameter("via");
if (text != null)
status = text + " ";
if (url != null)
status += url + " ";
if (via != null)
status += "via @" + via;
data.putString(StatusEditor.KEY_STATUS_EDITOR_TEXT, status);
return new LinkResult(data, StatusEditor.class);
}
}
// open hashtag search
// e.g. twitter.com/hashtag/{hashtag name}
else if (pathSeg.size() == 2 && pathSeg.get(0).equals("hashtag")) {
String search = '#' + pathSeg.get(1);
data.putString(SearchActivity.KEY_SEARCH_QUERY, search);
return new LinkResult(data, SearchActivity.class);
}
// open an userlist
// e.g. twitter.com/i/lists/{list id}
else if (pathSeg.size() == 3 && pathSeg.get(0).equals("i") && pathSeg.get(1).equals("lists") && pathSeg.get(2).matches("\\d+")) {
long listId = Long.parseLong(pathSeg.get(2));
UserList list = connection.getUserlist(listId);
data.putSerializable(UserlistActivity.KEY_LIST_DATA, list);
data.putBoolean(UserlistActivity.KEY_LIST_NO_UPDATE, true);
return new LinkResult(data, UserlistActivity.class);
}
// show status
// e.g. twitter.com/{screenname}/status/{tweet ID}
else if (pathSeg.size() == 3 && pathSeg.get(1).equals("status") && pathSeg.get(2).matches("\\d+")) {
String screenname = pathSeg.get(0);
long Id = Long.parseLong(pathSeg.get(2));
data.putLong(StatusActivity.KEY_STATUS_ID, Id);
data.putString(StatusActivity.KEY_STATUS_NAME, screenname);
return new LinkResult(data, StatusActivity.class);
}
// show userlists
// e.g. twitter.com/{screenname}/lists
else if (pathSeg.size() == 2 && pathSeg.get(1).equals("lists")) {
String screenname = pathSeg.get(0);
User user = connection.showUser(screenname);
data.putLong(UserlistsActivity.KEY_USERLIST_OWNER_ID, user.getId());
return new LinkResult(data, UserlistsActivity.class);
}
// show user profile
// e.g. twitter.com/{screenname}
else if (pathSeg.size() == 1 || (pathSeg.size() == 2 &&
(pathSeg.get(1).equals("with_replies") || pathSeg.get(1).equals("media") || pathSeg.get(1).equals("likes")))) {
String screenname = pathSeg.get(0);
User user = connection.showUser(screenname);
data.putSerializable(ProfileActivity.KEY_PROFILE_USER, user);
return new LinkResult(data, ProfileActivity.class);
}
}
} catch (ConnectionException exception) {
return new LinkResult(null, null, exception);
} catch (Exception e) {
e.printStackTrace();
}
return new LinkResult(null, null, null);
}
/**
* Holder class for information to start an activity
*/
public static class LinkResult {
@Nullable
public final Bundle data;
@Nullable
public final Class<? extends Activity> activity;
@Nullable
public final ConnectionException exception;
LinkResult(@NonNull Bundle data, @Nullable Class<? extends Activity> activity) {
this(data, activity, null);
}
LinkResult(@Nullable Bundle data, @Nullable Class<? extends Activity> activity, @Nullable ConnectionException exception) {
this.data = data;
this.activity = activity;
this.exception = exception;
}
}
}

View File

@ -1,51 +0,0 @@
package org.nuclearfog.twidda.backend.async;
import org.nuclearfog.twidda.ui.activities.VideoViewer;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* This class updates {@link VideoViewer}'s Seekbar while playing a video
*
* @author nuclearfog
*/
public class SeekbarUpdater implements Runnable {
private ScheduledExecutorService updater;
private WeakReference<VideoViewer> weakRef;
private Runnable seekUpdate = new Runnable() {
public void run() {
VideoViewer mediaViewer = weakRef.get();
if (mediaViewer != null) {
mediaViewer.updateSeekBar();
}
}
};
public SeekbarUpdater(VideoViewer activity, int milliseconds) {
weakRef = new WeakReference<>(activity);
updater = Executors.newScheduledThreadPool(1);
updater.scheduleWithFixedDelay(this, milliseconds, milliseconds, TimeUnit.MILLISECONDS);
}
@Override
public void run() {
VideoViewer mediaViewer = weakRef.get();
if (mediaViewer != null) {
mediaViewer.runOnUiThread(seekUpdate);
}
}
/**
* shutdown updater
*/
public void shutdown() {
updater.shutdown();
}
}

View File

@ -111,28 +111,6 @@ public class StringTools {
return resources.getString(R.string.time_now);
}
/**
* format media time to string
*
* @param time duration/current position in ms
* @return time string
*/
public static String formatMediaTime(int time) {
String result = "";
int seconds = (time / 1000) % 60;
int minutes = (time / 60000) % 60;
if (minutes < 10)
result += "0";
result += minutes + ":";
if (seconds < 10)
result += "0";
result += seconds;
return result;
}
/**
* un-escape html based text
*

View File

@ -90,7 +90,6 @@ public class GlobalSettings {
private static final String PROXY_PORT = "proxy_port";
private static final String PROXY_USER = "proxy_user";
private static final String PROXY_PASS = "proxy_pass";
private static final String PROXY_IGNORE = "ignore_proxy_set";
private static final String TREND_LOC = "location";
private static final String TREND_ID = "world_id_long";
private static final String ENABLE_LIKE = "like_enable";
@ -140,7 +139,6 @@ public class GlobalSettings {
private boolean loggedIn;
private boolean isProxyEnabled;
private boolean isProxyAuthSet;
private boolean ignoreProxyWarning;
private boolean toolbarOverlap;
private boolean tweetIndicators;
private boolean filterResults;
@ -860,27 +858,6 @@ public class GlobalSettings {
return isProxyAuthSet;
}
/**
* check if proxy warning should be ignored
*
* @return true if proxy warning should be ignored
*/
public boolean ignoreProxyWarning() {
return ignoreProxyWarning;
}
/**
* enable/ignore proxy warning
*
* @param ignore true to ignore proxy warning
*/
public void setIgnoreProxyWarning(boolean ignore) {
ignoreProxyWarning = ignore;
Editor e = settings.edit();
e.putBoolean(PROXY_IGNORE, ignore);
e.apply();
}
/**
* Check if current user is logged in
*
@ -986,7 +963,6 @@ public class GlobalSettings {
listSize = settings.getInt(LIST_SIZE, DEFAULT_LIST_SIZE);
isProxyEnabled = settings.getBoolean(PROXY_SET, false);
isProxyAuthSet = settings.getBoolean(AUTH_SET, false);
ignoreProxyWarning = settings.getBoolean(PROXY_IGNORE, false);
loggedIn = settings.getBoolean(LOGGED_IN, false);
loadImage = settings.getBoolean(IMAGE_LOAD, true);
tweetIndicators = settings.getBoolean(TWEET_INDICATOR, true);

View File

@ -30,11 +30,7 @@ import com.google.android.material.tabs.TabLayout.OnTabSelectedListener;
import com.google.android.material.tabs.TabLayout.Tab;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
import org.nuclearfog.twidda.backend.async.LinkLoader;
import org.nuclearfog.twidda.backend.async.LinkLoader.LinkResult;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.ui.adapter.FragmentAdapter;
import org.nuclearfog.twidda.ui.dialogs.ProgressDialog;
@ -44,13 +40,7 @@ import org.nuclearfog.twidda.ui.dialogs.ProgressDialog;
*
* @author nuclearfog
*/
public class MainActivity extends AppCompatActivity implements ActivityResultCallback<ActivityResult>, OnTabSelectedListener, OnQueryTextListener, AsyncCallback<LinkResult> {
/**
* key used to set the tab page
* vale type is Integer
*/
public static final String KEY_TAB_PAGE = "tab_pos";
public class MainActivity extends AppCompatActivity implements ActivityResultCallback<ActivityResult>, OnTabSelectedListener, OnQueryTextListener {
private ActivityResultLauncher<Intent> activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this);
@ -106,12 +96,6 @@ public class MainActivity extends AppCompatActivity implements ActivityResultCal
// initialize lists
else if (adapter.isEmpty()) {
setupAdapter(true);
// check if there is a Twitter link
if (getIntent().getData() != null) {
LinkLoader linkLoader = new LinkLoader(this);
linkLoader.execute(getIntent().getData(), this);
loadingCircle.show();
}
}
}
@ -257,29 +241,6 @@ public class MainActivity extends AppCompatActivity implements ActivityResultCal
adapter.scrollToTop(tab.getPosition());
}
@Override
public void onResult(@NonNull LinkResult linkResult) {
loadingCircle.dismiss();
if (linkResult.data != null && linkResult.activity != null) {
if (linkResult.activity == MainActivity.class) {
int page = linkResult.data.getInt(KEY_TAB_PAGE, 0);
pager.setCurrentItem(page);
} else {
Intent intent = new Intent(this, linkResult.activity);
intent.putExtras(linkResult.data);
startActivity(intent);
}
} else {
if (linkResult.exception != null) {
String message = ErrorHandler.getErrorMessage(this, linkResult.exception);
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), R.string.error_open_link, Toast.LENGTH_SHORT).show();
}
}
}
/**
* initialize pager content
*/

View File

@ -158,7 +158,7 @@ public class MessageEditor extends MediaActivity implements OnClickListener, OnC
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
// retry sending message
if (type == ConfirmDialog.MESSAGE_EDITOR_ERROR) {
sendMessage();

View File

@ -587,7 +587,7 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
if (user != null) {
// confirmed unfollowing user
if (type == ConfirmDialog.PROFILE_UNFOLLOW) {

View File

@ -239,7 +239,7 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, Asy
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
// leave without settings
if (type == ConfirmDialog.PROFILE_EDITOR_LEAVE) {
finish();

View File

@ -308,7 +308,7 @@ public class SettingsActivity extends AppCompatActivity implements OnClickListen
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
// confirm log out
if (type == ConfirmDialog.APP_LOG_OUT) {
// remove account from database
@ -692,7 +692,6 @@ public class SettingsActivity extends AppCompatActivity implements OnClickListen
}
} else {
settings.clearProxyServer();
settings.setIgnoreProxyWarning(false);
}
return checkPassed;
}

View File

@ -668,35 +668,16 @@ public class StatusActivity extends AppCompatActivity implements OnClickListener
@Override
public void onConfirm(int type, boolean rememberChoice) {
switch (type) {
case ConfirmDialog.DELETE_STATUS:
if (status != null) {
long id = status.getId();
if (status.getEmbeddedStatus() != null) {
id = status.getEmbeddedStatus().getId();
}
StatusParam param = new StatusParam(StatusParam.DELETE, id);
statusLoader.execute(param, statusCallback);
public void onConfirm(int type) {
if (type == ConfirmDialog.DELETE_STATUS) {
if (status != null) {
long id = status.getId();
if (status.getEmbeddedStatus() != null) {
id = status.getEmbeddedStatus().getId();
}
break;
case ConfirmDialog.PROXY_CONFIRM:
if (status != null) {
settings.setIgnoreProxyWarning(rememberChoice);
Media[] mediaItems = status.getMedia();
if (status.getEmbeddedStatus() != null) {
mediaItems = status.getEmbeddedStatus().getMedia();
}
if (mediaItems.length > 0) {
Uri uri = Uri.parse(mediaItems[0].getUrl());
Intent intent = new Intent(this, VideoViewer.class);
intent.putExtra(VideoViewer.VIDEO_URI, uri);
intent.putExtra(VideoViewer.ENABLE_VIDEO_CONTROLS, true);
startActivity(intent);
}
}
break;
StatusParam param = new StatusParam(StatusParam.DELETE, id);
statusLoader.execute(param, statusCallback);
}
}
}
@ -730,23 +711,15 @@ public class StatusActivity extends AppCompatActivity implements OnClickListener
mediaIntent.putExtra(ImageViewer.IMAGE_URI, uri);
startActivity(mediaIntent);
} else if (media.getMediaType() == Media.VIDEO) {
if (!settings.isProxyEnabled() || settings.ignoreProxyWarning()) {
Intent intent = new Intent(this, VideoViewer.class);
intent.putExtra(VideoViewer.VIDEO_URI, uri);
intent.putExtra(VideoViewer.ENABLE_VIDEO_CONTROLS, true);
startActivity(intent);
} else {
confirmDialog.show(ConfirmDialog.PROXY_CONFIRM);
}
Intent intent = new Intent(this, VideoViewer.class);
intent.putExtra(VideoViewer.VIDEO_URI, uri);
intent.putExtra(VideoViewer.ENABLE_VIDEO_CONTROLS, true);
startActivity(intent);
} else if (media.getMediaType() == Media.GIF) {
if (!settings.isProxyEnabled() || settings.ignoreProxyWarning()) {
Intent intent = new Intent(this, VideoViewer.class);
intent.putExtra(VideoViewer.VIDEO_URI, uri);
intent.putExtra(VideoViewer.ENABLE_VIDEO_CONTROLS, false);
startActivity(intent);
} else {
confirmDialog.show(ConfirmDialog.PROXY_CONFIRM);
}
Intent intent = new Intent(this, VideoViewer.class);
intent.putExtra(VideoViewer.VIDEO_URI, uri);
intent.putExtra(VideoViewer.ENABLE_VIDEO_CONTROLS, false);
startActivity(intent);
}
}

View File

@ -288,7 +288,7 @@ public class StatusEditor extends MediaActivity implements OnClickListener, OnPr
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
// retry uploading status
if (type == ConfirmDialog.STATUS_EDITOR_ERROR) {
updateStatus();

View File

@ -282,7 +282,7 @@ public class UserlistActivity extends AppCompatActivity implements ActivityResul
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
// delete user list
if (type == ConfirmDialog.LIST_DELETE && userList != null) {
if (listLoaderAsync.isIdle()) {

View File

@ -156,7 +156,7 @@ public class UserlistEditor extends AppCompatActivity implements OnClickListener
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
// retry updating list
if (type == ConfirmDialog.LIST_EDITOR_ERROR) {
updateList();

View File

@ -1,65 +1,42 @@
package org.nuclearfog.twidda.ui.activities;
import static android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START;
import static android.media.MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START;
import static android.media.MediaPlayer.OnCompletionListener;
import static android.media.MediaPlayer.OnErrorListener;
import static android.media.MediaPlayer.OnInfoListener;
import static android.media.MediaPlayer.OnPreparedListener;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.OnClickListener;
import static android.view.View.OnTouchListener;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.location.Location;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.MotionEvent;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.VideoView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.async.SeekbarUpdater;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.StringTools;
import org.nuclearfog.twidda.backend.utils.ConnectionBuilder;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog;
import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog.OnConfirmListener;
import okhttp3.Call;
/**
* video player activity to show local and online videos/animations
*
* @author nuclearfog
*/
public class VideoViewer extends MediaActivity implements OnSeekBarChangeListener, OnCompletionListener, OnDismissListener,
OnPreparedListener, OnInfoListener, OnErrorListener, OnClickListener, OnTouchListener, OnConfirmListener {
public class VideoViewer extends AppCompatActivity {
/**
* key for an Uri array with local links
@ -73,58 +50,10 @@ public class VideoViewer extends MediaActivity implements OnSeekBarChangeListene
*/
public static final String ENABLE_VIDEO_CONTROLS = "enable_controls";
/**
* playback status used if player is idle
*/
private static final int IDLE = -1;
/**
* playback status used for "play" mode
*/
private static final int PLAY = 1;
/**
* playback status used for "pause" mode
*/
private static final int PAUSE = 2;
/**
* playback status used for "forward" mode
*/
private static final int FORWARD = 3;
/**
* playback status used for "backward" mode
*/
private static final int BACKWARD = 4;
/**
* refresh time for video progress update in milliseconds
*/
private static final int PROGRESS_UPDATE = 1000;
/**
* speed ratio for "backward" and "forward"
*/
private static final int SPEED_FACTOR = 6;
@Nullable
private SeekbarUpdater seekUpdate;
private GlobalSettings settings;
private ExoPlayer player;
private ConfirmDialog confirmDialog;
private TextView duration, position;
private ProgressBar loadingCircle;
private SeekBar video_progress;
private ImageButton playPause;
private VideoView videoView;
private ViewGroup controlPanel;
private Uri link;
private boolean enableVideoExtras;
private int playStatus = IDLE;
private Uri data;
@Override
protected void attachBaseContext(Context newBase) {
@ -136,318 +65,68 @@ public class VideoViewer extends MediaActivity implements OnSeekBarChangeListene
protected void onCreate(@Nullable Bundle b) {
super.onCreate(b);
setContentView(R.layout.page_video);
ImageButton forward = findViewById(R.id.controller_forward);
ImageButton backward = findViewById(R.id.controller_backward);
ImageButton share = findViewById(R.id.controller_share);
loadingCircle = findViewById(R.id.media_progress);
controlPanel = findViewById(R.id.media_controlpanel);
videoView = findViewById(R.id.video_view);
video_progress = findViewById(R.id.controller_progress);
playPause = findViewById(R.id.controller_play);
duration = findViewById(R.id.controller_duration);
position = findViewById(R.id.controller_position);
confirmDialog = new ConfirmDialog(this);
ViewGroup root = findViewById(R.id.page_video_root);
StyledPlayerView playerView = findViewById(R.id.page_video_player);
Toolbar toolbar = findViewById(R.id.page_video_toolbar);
player = new ExoPlayer.Builder(this).build();
settings = GlobalSettings.getInstance(this);
AppStyles.setProgressColor(loadingCircle, settings.getHighlightColor());
AppStyles.setTheme(controlPanel);
videoView.setZOrderMediaOverlay(true); // disable black background
videoView.getHolder().setFormat(PixelFormat.TRANSPARENT);
// get extras
enableVideoExtras = getIntent().getBooleanExtra(ENABLE_VIDEO_CONTROLS, false);
Parcelable data = getIntent().getParcelableExtra(VIDEO_URI);
if (data instanceof Uri) {
link = (Uri) data;
// enable control bar if set
if (enableVideoExtras) {
controlPanel.setVisibility(VISIBLE);
if (link.getScheme().startsWith("http")) {
// attach link to share button
share.setTag(link);
} else {
share.setVisibility(GONE);
}
seekUpdate = new SeekbarUpdater(this, PROGRESS_UPDATE);
}
videoView.setVideoURI(link);
AppStyles.setTheme(root);
if (toolbar != null) {
setSupportActionBar(toolbar);
toolbar.setTitle("");
}
share.setOnClickListener(this);
playPause.setOnClickListener(this);
videoView.setOnTouchListener(this);
backward.setOnTouchListener(this);
forward.setOnTouchListener(this);
videoView.setOnPreparedListener(this);
videoView.setOnCompletionListener(this);
videoView.setOnErrorListener(this);
video_progress.setOnSeekBarChangeListener(this);
confirmDialog.setConfirmListener(this);
confirmDialog.setOnDismissListener(this);
}
@Override
protected void onStop() {
super.onStop();
if (enableVideoExtras) {
videoView.pause();
setPlayPause(R.drawable.play);
playStatus = PAUSE;
data = getIntent().getParcelableExtra(VIDEO_URI);
boolean enableControls = getIntent().getBooleanExtra(ENABLE_VIDEO_CONTROLS, true);
if (!enableControls) {
playerView.setUseController(false);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
}
MediaItem mediaItem = MediaItem.fromUri(data);
DataSource.Factory dataSourceFactory = new OkHttpDataSource.Factory((Call.Factory) ConnectionBuilder.create(this, 128000));
MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem);
player.setMediaSource(mediaSource);
playerView.setPlayer(player);
player.prepare();
player.play();
}
@Override
protected void onDestroy() {
if (seekUpdate != null)
seekUpdate.shutdown();
super.onDestroy();
protected void onPause() {
super.onPause();
player.pause();
}
@Override
public void onClick(View v) {
// play/pause video
if (v.getId() == R.id.controller_play) {
if (playStatus == PAUSE) {
playStatus = PLAY;
if (!videoView.isPlaying())
videoView.start();
setPlayPause(R.drawable.pause);
} else if (playStatus == PLAY) {
playStatus = PAUSE;
if (videoView.isPlaying())
videoView.pause();
setPlayPause(R.drawable.play);
}
}
// open link with another app
else if (v.getId() == R.id.controller_share) {
if (v.getTag() instanceof Uri) {
Intent intent = new Intent(Intent.ACTION_VIEW, (Uri) v.getTag());
try {
startActivity(intent);
} catch (ActivityNotFoundException err) {
Toast.makeText(getApplicationContext(), R.string.error_connection_failed, LENGTH_SHORT).show();
}
}
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.video, menu);
AppStyles.setMenuIconColor(menu, settings.getIconColor());
menu.findItem(R.id.menu_video_link).setVisible(data.getScheme().startsWith("http"));
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// fast backward
if (v.getId() == R.id.controller_backward) {
if (playStatus == PAUSE)
return false;
if (event.getAction() == ACTION_DOWN) {
playStatus = BACKWARD;
videoView.pause();
return true;
}
if (event.getAction() == ACTION_UP) {
playStatus = PLAY;
videoView.start();
return true;
}
}
// fast forward
else if (v.getId() == R.id.controller_forward) {
if (playStatus == PAUSE)
return false;
if (event.getAction() == ACTION_DOWN) {
playStatus = FORWARD;
videoView.pause();
return true;
}
if (event.getAction() == ACTION_UP) {
playStatus = PLAY;
videoView.start();
return true;
}
}
// show/hide control panel
else if (v.getId() == R.id.video_view) {
if (event.getAction() == ACTION_DOWN) {
if (enableVideoExtras) {
if (controlPanel.getVisibility() == VISIBLE) {
controlPanel.setVisibility(INVISIBLE);
} else {
controlPanel.setVisibility(VISIBLE);
}
return true;
}
}
}
return false;
}
@Override
protected void onAttachLocation(@Nullable Location location) {
}
@Override
protected void onMediaFetched(int resultType, @NonNull Uri uri) {
}
@Override
public void onPrepared(MediaPlayer mp) {
// enable controls for video
if (enableVideoExtras) {
switch (playStatus) {
case IDLE:
// initialize seekbar
playStatus = PLAY;
video_progress.setMax(mp.getDuration());
duration.setText(StringTools.formatMediaTime(mp.getDuration()));
mp.setOnInfoListener(this);
// fall through
case PLAY:
// set video pos and start playback
mp.seekTo(video_progress.getProgress());
mp.start();
break;
case PAUSE:
// set only video pos
mp.seekTo(video_progress.getProgress());
break;
}
}
// setup video looping for gif
else {
// disable audiofocus for gif
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
videoView.setAudioFocusRequest(AudioManager.AUDIOFOCUS_NONE);
}
loadingCircle.setVisibility(INVISIBLE);
mp.setLooping(true);
mp.start();
}
}
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
switch (what) {
case MEDIA_INFO_BUFFERING_END:
case MEDIA_INFO_VIDEO_RENDERING_START:
loadingCircle.setVisibility(INVISIBLE);
return true;
case MEDIA_INFO_BUFFERING_START:
loadingCircle.setVisibility(VISIBLE);
return true;
}
return false;
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
if (what == MEDIA_ERROR_UNKNOWN) {
if (link.getScheme().startsWith("http")) {
confirmDialog.show(ConfirmDialog.VIDEO_ERROR);
} else {
Toast.makeText(getApplicationContext(), R.string.error_cant_load_video, LENGTH_SHORT).show();
finish();
}
return true;
}
return false;
}
@Override
public void onCompletion(MediaPlayer mp) {
setPlayPause(R.drawable.play);
video_progress.setProgress(0);
playStatus = PAUSE;
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
videoView.pause();
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
position.setText(StringTools.formatMediaTime(progress));
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
videoView.seekTo(seekBar.getProgress());
if (playStatus == PLAY) {
videoView.start();
}
}
@Override
public void onConfirm(int type, boolean rememberChoice) {
if (type == ConfirmDialog.VIDEO_ERROR) {
if (link != null) {
// open link in a browser
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_video_link) {
if (data != null) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(link);
intent.setData(data);
try {
startActivity(intent);
} catch (ActivityNotFoundException err) {
Toast.makeText(getApplicationContext(), R.string.error_connection_failed, LENGTH_SHORT).show();
Toast.makeText(getApplicationContext(), R.string.error_connection_failed, Toast.LENGTH_SHORT).show();
}
}
}
}
@Override
public void onDismiss(DialogInterface dialog) {
finish();
}
/**
* updates controller panel SeekBar
*/
public void updateSeekBar() {
int videoPos = video_progress.getProgress();
switch (playStatus) {
case PLAY:
video_progress.setProgress(videoView.getCurrentPosition());
break;
case FORWARD:
videoPos += 2 * PROGRESS_UPDATE * SPEED_FACTOR;
if (videoPos > videoView.getDuration())
videoPos = videoView.getDuration();
videoView.seekTo(videoPos);
video_progress.setProgress(videoPos);
break;
case BACKWARD:
videoPos -= 2 * PROGRESS_UPDATE * SPEED_FACTOR;
if (videoPos < 0)
videoPos = 0;
videoView.seekTo(videoPos);
video_progress.setProgress(videoPos);
break;
}
}
private void setPlayPause(@DrawableRes int icon) {
playPause.setImageResource(icon);
AppStyles.setDrawableColor(playPause.getDrawable(), settings.getIconColor());
AppStyles.setButtonColor(playPause, settings.getFontColor());
return super.onOptionsItemSelected(item);
}
}

View File

@ -6,7 +6,6 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
@ -139,8 +138,7 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
public static final int NOTIFICATION_DISMISS = 623;
private TextView title, message, confirmDescr;
private CompoundButton confirmCheck;
private TextView title, message;
private Button confirm, cancel;
private ViewGroup root;
@ -158,8 +156,6 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
cancel = findViewById(R.id.confirm_no);
title = findViewById(R.id.confirm_title);
message = findViewById(R.id.confirm_message);
confirmDescr = findViewById(R.id.confirm_remember_descr);
confirmCheck = findViewById(R.id.confirm_remember);
confirm.setOnClickListener(this);
cancel.setOnClickListener(this);
@ -193,7 +189,6 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
confirm.setTag(type);
// default visibility values
int titleVis = View.GONE;
int confirmVis = View.INVISIBLE;
int cancelVis = View.VISIBLE;
// default resource values
int titleRes = R.string.info_error;
@ -288,7 +283,6 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
break;
case PROXY_CONFIRM:
confirmVis = View.VISIBLE;
titleVis = View.VISIBLE;
titleRes = R.string.dialog_confirm_warning;
messageRes = R.string.dialog_warning_videoview;
@ -304,9 +298,6 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
// setup confirm button
confirm.setText(confirmRes);
confirm.setCompoundDrawablesWithIntrinsicBounds(confirmIconRes, 0, 0, 0);
// setup remember choice checkbox
confirmCheck.setVisibility(confirmVis);
confirmDescr.setVisibility(confirmVis);
// setup message
if (messageTxt.isEmpty()) {
message.setText(messageRes);
@ -324,8 +315,7 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
Object tag = v.getTag();
if (listener != null && tag instanceof Integer) {
int type = (int) tag;
boolean remember = confirmCheck.getVisibility() == View.VISIBLE && confirmCheck.isChecked();
listener.onConfirm(type, remember);
listener.onConfirm(type);
}
dismiss();
} else if (v.getId() == R.id.confirm_no) {
@ -348,9 +338,8 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
/**
* called when the positive button was clicked
*
* @param type type of dialog
* @param rememberChoice true if choice should be remembered
* @param type type of dialog
*/
void onConfirm(int type, boolean rememberChoice);
void onConfirm(int type);
}
}

View File

@ -105,7 +105,7 @@ public class AccountFragment extends ListFragment implements OnAccountClickListe
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
if (type == ConfirmDialog.REMOVE_ACCOUNT) {
load(AccountParameter.DELETE);
}

View File

@ -177,7 +177,7 @@ public class MessageFragment extends ListFragment implements OnMessageClickListe
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
if (type == ConfirmDialog.MESSAGE_DELETE) {
MessageLoaderParam param = new MessageLoaderParam(MessageLoaderParam.DELETE, 0, selectedId, "");
messageLoader.execute(param, this);

View File

@ -152,7 +152,7 @@ public class NotificationFragment extends ListFragment implements OnNotification
@Override
public void onConfirm(int type, boolean rememberChoice) {
public void onConfirm(int type) {
if (type == ConfirmDialog.NOTIFICATION_DISMISS) {
if (select != null) {
NotificationActionParam param = new NotificationActionParam(NotificationActionParam.DISMISS, select.getId());

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M17.959,4.571L10.756,9.52c0,0 -0.279,0.201 -0.279,0.481s0.279,0.479 0.279,0.479l7.203,4.951C18.531,15.811 19,15.53 19,14.805V5.196C19,4.469 18.531,4.188 17.959,4.571zM8.883,4.571L1.68,9.52c0,0 -0.279,0.201 -0.279,0.481S1.68,10.48 1.68,10.48l7.203,4.951c0.572,0.381 1.041,0.1 1.041,-0.625v-9.61C9.924,4.469 9.455,4.188 8.883,4.571z"
android:fillColor="#FFFFFF" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9.244,9.52L2.041,4.571C1.469,4.188 1,4.469 1,5.196v9.609c0,0.725 0.469,1.006 1.041,0.625l7.203,-4.951c0,0 0.279,-0.199 0.279,-0.478C9.523,9.721 9.244,9.52 9.244,9.52zM18.6,10.001c0,0.279 -0.279,0.478 -0.279,0.478l-7.203,4.951c-0.572,0.381 -1.041,0.1 -1.041,-0.625V5.196c0,-0.727 0.469,-1.008 1.041,-0.625L18.32,9.52C18.32,9.52 18.6,9.721 18.6,10.001z"
android:fillColor="#FFFFFF" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M15,3h-2c-0.553,0 -1,0.048 -1,0.6v12.8c0,0.552 0.447,0.6 1,0.6h2c0.553,0 1,-0.048 1,-0.6V3.6C16,3.048 15.553,3 15,3zM7,3H5C4.447,3 4,3.048 4,3.6v12.8C4,16.952 4.447,17 5,17h2c0.553,0 1,-0.048 1,-0.6V3.6C8,3.048 7.553,3 7,3z"
android:fillColor="#FFFFFF" />
</vector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/page_video_root"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/page_video_toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/page_status_toolbar_height"
android:visibility="gone"/>
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/page_video_player"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

View File

@ -39,30 +39,8 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="top"
app:constraint_referenced_ids="confirm_no,confirm_yes,confirm_remember,confirm_remember_descr" />
<CheckBox
android:id="@+id/confirm_remember"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/confirm_button_margin"
android:lines="1"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/confirm_remember_descr"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/confirm_remember_descr"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/confirm_checkbox_text_margin"
android:text="@string/dialog_confirm_remember"
android:textSize="@dimen/confirm_checkbox_text_size"
app:layout_constraintBottom_toBottomOf="@id/confirm_remember"
app:layout_constraintEnd_toStartOf="@id/confirm_no"
app:layout_constraintStart_toEndOf="@id/confirm_remember"
app:layout_constraintTop_toTopOf="@id/confirm_remember" />
app:constraint_referenced_ids="confirm_no,confirm_yes"
tools:layout_editor_absoluteY="96dp" />
<Button
android:id="@+id/confirm_no"

View File

@ -1,135 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/page_video_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true"
tools:context=".ui.activities.VideoViewer">
<VideoView
android:id="@+id/video_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/page_video_toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/page_status_toolbar_height" />
<ProgressBar
android:id="@+id/media_progress"
android:layout_width="@dimen/mediapage_circle_size"
android:layout_height="@dimen/mediapage_circle_size"
android:layout_marginTop="@dimen/mediapage_preview_margin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/page_video_player"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/media_controlpanel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
android:layout_marginStart="@dimen/mediapage_controller_layout_margin"
android:layout_marginBottom="@dimen/mediapage_controller_layout_margin"
android:layout_marginEnd="@dimen/mediapage_controller_layout_margin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageButton
android:id="@+id/controller_backward"
android:layout_width="@dimen/controller_button_width"
android:layout_height="@dimen/controller_button_height"
android:layout_marginTop="@dimen/controller_seekbar_margin"
android:contentDescription="@string/button_backward"
android:src="@drawable/backward"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/controller_play"
style="@style/RoundButton" />
<ImageButton
android:id="@+id/controller_play"
android:layout_width="@dimen/controller_button_width"
android:layout_height="@dimen/controller_button_height"
android:layout_marginTop="@dimen/controller_seekbar_margin"
android:contentDescription="@string/button_play_pause"
android:src="@drawable/pause"
app:layout_constraintStart_toEndOf="@id/controller_backward"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/controller_forward"
style="@style/RoundButton" />
<ImageButton
android:id="@+id/controller_forward"
android:layout_width="@dimen/controller_button_width"
android:layout_height="@dimen/controller_button_height"
android:layout_marginTop="@dimen/controller_seekbar_margin"
android:contentDescription="@string/button_forward"
android:src="@drawable/forward"
app:layout_constraintStart_toEndOf="@id/controller_play"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/controller_share"
style="@style/RoundButton" />
<ImageButton
android:id="@+id/controller_share"
android:layout_width="@dimen/controller_button_width"
android:layout_height="@dimen/controller_button_height"
android:layout_marginTop="@dimen/controller_seekbar_margin"
android:contentDescription="@string/button_share"
android:src="@drawable/share"
app:layout_constraintStart_toEndOf="@id/controller_forward"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
style="@style/RoundButton" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/controller_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="controller_backward,controller_forward,controller_play" />
<TextView
android:id="@+id/controller_position"
android:layout_width="@dimen/controller_text_width"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/controller_text_margin"
android:lines="1"
android:textSize="@dimen/controller_text_size"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/controller_progress"
app:layout_constraintBottom_toBottomOf="@id/controller_progress"
app:layout_constraintEnd_toStartOf="@id/controller_progress" />
<SeekBar
android:id="@+id/controller_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/controller_seekbar_margin"
android:layout_marginBottom="@dimen/controller_seekbar_margin"
android:contentDescription="@string/button_backward"
app:layout_constraintStart_toEndOf="@id/controller_position"
app:layout_constraintTop_toBottomOf="@id/controller_barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/controller_duration" />
<TextView
android:id="@+id/controller_duration"
android:layout_width="@dimen/controller_text_width"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/controller_text_margin"
android:lines="1"
android:textSize="@dimen/controller_text_size"
app:layout_constraintStart_toEndOf="@id/controller_progress"
app:layout_constraintTop_toTopOf="@id/controller_progress"
app:layout_constraintBottom_toBottomOf="@id/controller_progress"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_video_link"
android:icon="@drawable/share"
android:title="@string/button_share"
app:showAsAction="always" />
</menu>

View File

@ -183,9 +183,6 @@
<string name="error_api_key_expired">Fehler, API Schlüssel veraltet, bitte App aktualisieren!</string>
<string name="info_location_pending">Ortung läuft, bitte warten.</string>
<string name="error_twitter_search">Suchbegriff ist entweder zu lang oder enthält nicht erlaubte Zeichen!</string>
<string name="button_backward">Zurückspulen</string>
<string name="button_forward">Vorspulen</string>
<string name="button_play_pause">Pause/Abspielen</string>
<string name="button_share">Videolink teilen</string>
<string name="settings_look">Aussehen</string>
<string name="dialog_link_image_preview">Linkvorschau Bild</string>
@ -237,7 +234,6 @@
<string name="dialog_confirm_warning">Warnung</string>
<string name="dialog_warning_videoview">Videoplayer unterstützt keine Proxy Verbindung! Fortsetzen ohne Proxy?</string>
<string name="confirm_unknown_error">Unbekannter Fehler!</string>
<string name="dialog_confirm_remember">merken</string>
<string name="error_invalid_media">Ungültige Mediendatei!</string>
<string name="menu_follow_requests">Anfragen</string>
<string name="menu_toolbar_excluded_users">Blocklisten</string>

View File

@ -83,9 +83,6 @@
<string name="info_list_followed">已关注列表</string>
<string name="info_list_unfollowed">已取消关注列表</string>
<string name="descr_add_profile_image">更换头像</string>
<string name="button_backward">后退</string>
<string name="button_forward">快进</string>
<string name="button_play_pause">暂停/播放</string>
<string name="button_share">分享链接</string>
<!-- toast messages to inform user -->

View File

@ -187,7 +187,6 @@
<dimen name="mediapage_toolbar_height">@dimen/toolbar_height</dimen>
<dimen name="mediapage_preview_margin">10dp</dimen>
<dimen name="mediapage_circle_size">64dp</dimen>
<dimen name="mediapage_controller_layout_margin">10dp</dimen>
<!--dimens of item_placeholder.xml-->
<dimen name="item_placeholder_margin">8dp</dimen>
@ -227,8 +226,6 @@
<dimen name="confirm_message_fontsize">18sp</dimen>
<dimen name="confirm_title_fontsize">22sp</dimen>
<dimen name="confirm_button_height">30sp</dimen>
<dimen name="confirm_checkbox_text_margin">5dp</dimen>
<dimen name="confirm_checkbox_text_size">10sp</dimen>
<integer name="confirm_message_max_ines">8</integer>
<!--dimens of dialog_userlist.xml-->
@ -258,14 +255,6 @@
<dimen name="tabitem_icon_size">22sp</dimen>
<dimen name="tabitem_textsize">11sp</dimen>
<!--dimens of controlpanel-->
<dimen name="controller_seekbar_margin">10dp</dimen>
<dimen name="controller_text_margin">5dp</dimen>
<dimen name="controller_text_size">11sp</dimen>
<dimen name="controller_button_width">64dp</dimen>
<dimen name="controller_button_height">32dp</dimen>
<dimen name="controller_text_width">36sp</dimen>
<!--dimens of item_card.xml-->
<dimen name="item_card_textsize">11sp</dimen>
<dimen name="item_card_layout_padding">5dp</dimen>

View File

@ -283,13 +283,9 @@
<string name="update_list">update list</string>
<string name="confirm_remove_user_from_list">remove user from list?</string>
<string name="descr_remove_user">remove user from list</string>
<string name="dialog_confirm_remember">remember</string>
<string name="confirm_unknown_error">unknown error!</string>
<string name="list_following_indicator">following list</string>
<string name="descr_add_profile_image">change profile image</string>
<string name="button_backward">Rewind</string>
<string name="button_forward">Fast forward</string>
<string name="button_play_pause">Pause/Play</string>
<string name="button_share">share link</string>
<string name="settings_rt_icon_color">Reposts</string>
<string name="settings_follow_req_color">Follow request</string>

View File

@ -10,7 +10,6 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath 'io.michaelrocks:paranoid-gradle-plugin:0.3.7' // fixme Find replacement
classpath 'gradle.plugin.ru.cleverpumpkin.proguard-dictionaries-generator:plugin:1.0.8'
}
}