v0.4.3: implemented a new 'Power Saver' mode
As a bonus, this allows shattered to run on small screen devices, by forcing power saver, which downsamples in this specific case allowing for the minimum 2x game scale.
This commit is contained in:
parent
8591a0b3dc
commit
debbb57066
|
@ -24,6 +24,7 @@ package com.watabou.input;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import com.watabou.noosa.Game;
|
||||||
import com.watabou.utils.PointF;
|
import com.watabou.utils.PointF;
|
||||||
import com.watabou.utils.Signal;
|
import com.watabou.utils.Signal;
|
||||||
|
|
||||||
|
@ -97,6 +98,9 @@ public class Touchscreen {
|
||||||
float x = e.getX( index );
|
float x = e.getX( index );
|
||||||
float y = e.getY( index );
|
float y = e.getY( index );
|
||||||
|
|
||||||
|
x /= (Game.dispWidth / (float)Game.width);
|
||||||
|
y /= (Game.dispHeight / (float)Game.height);
|
||||||
|
|
||||||
start = new PointF( x, y );
|
start = new PointF( x, y );
|
||||||
current = new PointF( x, y );
|
current = new PointF( x, y );
|
||||||
|
|
||||||
|
@ -104,7 +108,13 @@ public class Touchscreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update( MotionEvent e, int index ) {
|
public void update( MotionEvent e, int index ) {
|
||||||
current.set( e.getX( index ), e.getY( index ) );
|
float x = e.getX( index );
|
||||||
|
float y = e.getY( index );
|
||||||
|
|
||||||
|
x /= (Game.dispWidth / (float)Game.width);
|
||||||
|
y /= (Game.dispHeight / (float)Game.height);
|
||||||
|
|
||||||
|
current.set( x, y );
|
||||||
}
|
}
|
||||||
|
|
||||||
public Touch up() {
|
public Touch up() {
|
||||||
|
|
|
@ -54,7 +54,11 @@ public class Game extends Activity implements GLSurfaceView.Renderer, View.OnTou
|
||||||
|
|
||||||
public static Game instance;
|
public static Game instance;
|
||||||
|
|
||||||
// Actual size of the screen
|
//actual size of the display
|
||||||
|
public static int dispWidth;
|
||||||
|
public static int dispHeight;
|
||||||
|
|
||||||
|
// Size of the EGL surface view
|
||||||
public static int width;
|
public static int width;
|
||||||
public static int height;
|
public static int height;
|
||||||
|
|
||||||
|
@ -107,6 +111,8 @@ public class Game extends Activity implements GLSurfaceView.Renderer, View.OnTou
|
||||||
DisplayMetrics m = new DisplayMetrics();
|
DisplayMetrics m = new DisplayMetrics();
|
||||||
getWindowManager().getDefaultDisplay().getMetrics( m );
|
getWindowManager().getDefaultDisplay().getMetrics( m );
|
||||||
density = m.density;
|
density = m.density;
|
||||||
|
dispHeight = m.heightPixels;
|
||||||
|
dispWidth = m.widthPixels;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
version = getPackageManager().getPackageInfo( getPackageName(), 0 ).versionName;
|
version = getPackageManager().getPackageInfo( getPackageName(), 0 ).versionName;
|
||||||
|
|
|
@ -14,11 +14,13 @@
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:glEsVersion="0x00020000"/>
|
android:glEsVersion="0x00020000"/>
|
||||||
|
|
||||||
|
<!-- Note that the game doesn't truly support small screen resolutions,
|
||||||
|
it instead forces downscaling to work on these displays.-->
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:smallScreens="false"
|
android:smallScreens="true"
|
||||||
android:normalScreens="true"
|
android:normalScreens="true"
|
||||||
android:largeScreens="true"/>
|
android:largeScreens="true"
|
||||||
<!--android:xlargeScreens="true"-->
|
android:xlargeScreens="true"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
|
|
|
@ -30,6 +30,7 @@ enum Preferences {
|
||||||
|
|
||||||
public static final String KEY_LANDSCAPE = "landscape";
|
public static final String KEY_LANDSCAPE = "landscape";
|
||||||
public static final String KEY_IMMERSIVE = "immersive";
|
public static final String KEY_IMMERSIVE = "immersive";
|
||||||
|
public static final String KEY_POWER_SAVER = "power_saver";
|
||||||
public static final String KEY_SCALE = "scale";
|
public static final String KEY_SCALE = "scale";
|
||||||
public static final String KEY_MUSIC = "music";
|
public static final String KEY_MUSIC = "music";
|
||||||
public static final String KEY_MUSIC_VOL = "music_vol";
|
public static final String KEY_MUSIC_VOL = "music_vol";
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
*/
|
*/
|
||||||
package com.shatteredpixel.shatteredpixeldungeon;
|
package com.shatteredpixel.shatteredpixeldungeon;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -34,6 +34,7 @@ import com.watabou.noosa.Game;
|
||||||
import com.watabou.noosa.RenderedText;
|
import com.watabou.noosa.RenderedText;
|
||||||
import com.watabou.noosa.audio.Music;
|
import com.watabou.noosa.audio.Music;
|
||||||
import com.watabou.noosa.audio.Sample;
|
import com.watabou.noosa.audio.Sample;
|
||||||
|
import com.watabou.utils.GameMath;
|
||||||
|
|
||||||
import javax.microedition.khronos.opengles.GL10;
|
import javax.microedition.khronos.opengles.GL10;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -172,7 +173,10 @@ public class ShatteredPixelDungeon extends Game {
|
||||||
updateImmersiveMode();
|
updateImmersiveMode();
|
||||||
|
|
||||||
DisplayMetrics metrics = new DisplayMetrics();
|
DisplayMetrics metrics = new DisplayMetrics();
|
||||||
instance.getWindowManager().getDefaultDisplay().getMetrics( metrics );
|
if (immersed() && Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||||
|
getWindowManager().getDefaultDisplay().getRealMetrics( metrics );
|
||||||
|
else
|
||||||
|
getWindowManager().getDefaultDisplay().getMetrics( metrics );
|
||||||
boolean landscape = metrics.widthPixels > metrics.heightPixels;
|
boolean landscape = metrics.widthPixels > metrics.heightPixels;
|
||||||
|
|
||||||
if (Preferences.INSTANCE.getBoolean( Preferences.KEY_LANDSCAPE, false ) != landscape) {
|
if (Preferences.INSTANCE.getBoolean( Preferences.KEY_LANDSCAPE, false ) != landscape) {
|
||||||
|
@ -274,6 +278,7 @@ public class ShatteredPixelDungeon extends Game {
|
||||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||||
}
|
}
|
||||||
Preferences.INSTANCE.put( Preferences.KEY_LANDSCAPE, value );
|
Preferences.INSTANCE.put( Preferences.KEY_LANDSCAPE, value );
|
||||||
|
((ShatteredPixelDungeon)instance).updateDisplaySize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean landscape() {
|
public static boolean landscape() {
|
||||||
|
@ -284,11 +289,8 @@ public class ShatteredPixelDungeon extends Game {
|
||||||
Preferences.INSTANCE.put( Preferences.KEY_SCALE, value );
|
Preferences.INSTANCE.put( Preferences.KEY_SCALE, value );
|
||||||
}
|
}
|
||||||
|
|
||||||
// *** IMMERSIVE MODE ****
|
|
||||||
|
|
||||||
private static boolean immersiveModeChanged = false;
|
private static boolean immersiveModeChanged = false;
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
public static void immerse( boolean value ) {
|
public static void immerse( boolean value ) {
|
||||||
Preferences.INSTANCE.put( Preferences.KEY_IMMERSIVE, value );
|
Preferences.INSTANCE.put( Preferences.KEY_IMMERSIVE, value );
|
||||||
|
|
||||||
|
@ -297,21 +299,78 @@ public class ShatteredPixelDungeon extends Game {
|
||||||
public void run() {
|
public void run() {
|
||||||
updateImmersiveMode();
|
updateImmersiveMode();
|
||||||
immersiveModeChanged = true;
|
immersiveModeChanged = true;
|
||||||
|
//ensures surfacechanged is called if the view was previously set to be fixed.
|
||||||
|
((ShatteredPixelDungeon)instance).view.getHolder().setSizeFromLayout();
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSurfaceChanged( GL10 gl, int width, int height ) {
|
public void onSurfaceChanged( GL10 gl, int width, int height ) {
|
||||||
|
|
||||||
super.onSurfaceChanged( gl, width, height );
|
super.onSurfaceChanged( gl, width, height );
|
||||||
|
|
||||||
|
updateDisplaySize();
|
||||||
|
|
||||||
if (immersiveModeChanged) {
|
if (immersiveModeChanged) {
|
||||||
requestedReset = true;
|
requestedReset = true;
|
||||||
immersiveModeChanged = false;
|
immersiveModeChanged = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
private void updateDisplaySize(){
|
||||||
|
DisplayMetrics m = new DisplayMetrics();
|
||||||
|
if (immersed() && Build.VERSION.SDK_INT >= 19)
|
||||||
|
getWindowManager().getDefaultDisplay().getRealMetrics( m );
|
||||||
|
else
|
||||||
|
getWindowManager().getDefaultDisplay().getMetrics( m );
|
||||||
|
dispHeight = m.heightPixels;
|
||||||
|
dispWidth = m.widthPixels;
|
||||||
|
|
||||||
|
float dispRatio = dispWidth / (float)dispHeight;
|
||||||
|
|
||||||
|
float renderWidth = dispRatio > 1 ? PixelScene.MIN_WIDTH_L : PixelScene.MIN_WIDTH_P;
|
||||||
|
float renderHeight = dispRatio > 1 ? PixelScene.MIN_HEIGHT_L : PixelScene.MIN_HEIGHT_P;
|
||||||
|
|
||||||
|
//force power saver in this case as all devices must run at at least 2x scale.
|
||||||
|
if (dispWidth < renderWidth*2 || dispHeight < renderHeight*2)
|
||||||
|
Preferences.INSTANCE.put( Preferences.KEY_POWER_SAVER, true );
|
||||||
|
|
||||||
|
if (powerSaver()){
|
||||||
|
|
||||||
|
int maxZoom = (int)Math.min(dispWidth/renderWidth, dispHeight/renderHeight);
|
||||||
|
|
||||||
|
renderWidth *= GameMath.gate( 2, (float)Math.ceil(maxZoom/2f), 4);
|
||||||
|
renderHeight *= GameMath.gate( 2, (float)Math.ceil(maxZoom/2f), 4);
|
||||||
|
|
||||||
|
if (dispRatio > renderWidth / renderHeight){
|
||||||
|
renderWidth = renderHeight * dispRatio;
|
||||||
|
} else {
|
||||||
|
renderHeight = renderWidth / dispRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int finalW = Math.round(renderWidth);
|
||||||
|
final int finalH = Math.round(renderHeight);
|
||||||
|
if (finalW != width || finalH != height){
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
view.getHolder().setFixedSize(finalW, finalH);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
view.getHolder().setSizeFromLayout();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void updateImmersiveMode() {
|
public static void updateImmersiveMode() {
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 19) {
|
if (android.os.Build.VERSION.SDK_INT >= 19) {
|
||||||
try {
|
try {
|
||||||
|
@ -336,7 +395,14 @@ public class ShatteredPixelDungeon extends Game {
|
||||||
return Preferences.INSTANCE.getBoolean( Preferences.KEY_IMMERSIVE, false );
|
return Preferences.INSTANCE.getBoolean( Preferences.KEY_IMMERSIVE, false );
|
||||||
}
|
}
|
||||||
|
|
||||||
// *****************************
|
public static boolean powerSaver(){
|
||||||
|
return Preferences.INSTANCE.getBoolean( Preferences.KEY_POWER_SAVER, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void powerSaver( boolean value ){
|
||||||
|
Preferences.INSTANCE.put( Preferences.KEY_POWER_SAVER, value );
|
||||||
|
((ShatteredPixelDungeon)instance).updateDisplaySize();
|
||||||
|
}
|
||||||
|
|
||||||
public static int scale() {
|
public static int scale() {
|
||||||
return Preferences.INSTANCE.getInt( Preferences.KEY_SCALE, 0 );
|
return Preferences.INSTANCE.getInt( Preferences.KEY_SCALE, 0 );
|
||||||
|
|
|
@ -54,6 +54,7 @@ public class PixelScene extends Scene {
|
||||||
|
|
||||||
public static int defaultZoom = 0;
|
public static int defaultZoom = 0;
|
||||||
public static int maxDefaultZoom = 0;
|
public static int maxDefaultZoom = 0;
|
||||||
|
public static int maxScreenZoom = 0;
|
||||||
public static float minZoom;
|
public static float minZoom;
|
||||||
public static float maxZoom;
|
public static float maxZoom;
|
||||||
|
|
||||||
|
@ -82,6 +83,7 @@ public class PixelScene extends Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
maxDefaultZoom = (int)Math.min(Game.width/minWidth, Game.height/minHeight);
|
maxDefaultZoom = (int)Math.min(Game.width/minWidth, Game.height/minHeight);
|
||||||
|
maxScreenZoom = (int)Math.min(Game.dispWidth/minWidth, Game.dispHeight/minHeight);
|
||||||
defaultZoom = ShatteredPixelDungeon.scale();
|
defaultZoom = ShatteredPixelDungeon.scale();
|
||||||
|
|
||||||
if (defaultZoom < Math.ceil( Game.density * 2 ) || defaultZoom > maxDefaultZoom){
|
if (defaultZoom < Math.ceil( Game.density * 2 ) || defaultZoom > maxDefaultZoom){
|
||||||
|
|
|
@ -54,6 +54,12 @@ public abstract class OptionSlider extends Component {
|
||||||
public OptionSlider(String title, String minTxt, String maxTxt, int minVal, int maxVal){
|
public OptionSlider(String title, String minTxt, String maxTxt, int minVal, int maxVal){
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
//shouldn't function if this happens.
|
||||||
|
if (minVal > maxVal){
|
||||||
|
minVal = maxVal;
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.title.text(title);
|
this.title.text(title);
|
||||||
this.minTxt.text(minTxt);
|
this.minTxt.text(minTxt);
|
||||||
this.maxTxt.text(maxTxt);
|
this.maxTxt.text(maxTxt);
|
||||||
|
@ -61,12 +67,6 @@ public abstract class OptionSlider extends Component {
|
||||||
this.minVal = minVal;
|
this.minVal = minVal;
|
||||||
this.maxVal = maxVal;
|
this.maxVal = maxVal;
|
||||||
|
|
||||||
//really shouldn't display the slider if this happens.
|
|
||||||
if (minVal > maxVal){
|
|
||||||
active = false;
|
|
||||||
visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sliderTicks = new ColorBlock[(maxVal - minVal) + 1];
|
sliderTicks = new ColorBlock[(maxVal - minVal) + 1];
|
||||||
for (int i = 0; i < sliderTicks.length; i++){
|
for (int i = 0; i < sliderTicks.length; i++){
|
||||||
add(sliderTicks[i] = new ColorBlock(1, 11, 0xFF222222));
|
add(sliderTicks[i] = new ColorBlock(1, 11, 0xFF222222));
|
||||||
|
|
|
@ -127,12 +127,9 @@ public class WndSettings extends WndTabbed {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
scale.setSelectedValue(PixelScene.defaultZoom);
|
scale.setSelectedValue(PixelScene.defaultZoom);
|
||||||
if ((int)Math.ceil(2* Game.density) < PixelScene.maxDefaultZoom) {
|
|
||||||
scale.setRect(0, 0, WIDTH, SLIDER_HEIGHT);
|
scale.setRect(0, 0, WIDTH, SLIDER_HEIGHT);
|
||||||
|
if ((int)Math.ceil(2* Game.density) < PixelScene.maxDefaultZoom)
|
||||||
add(scale);
|
add(scale);
|
||||||
} else {
|
|
||||||
scale.setRect(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
OptionSlider brightness = new OptionSlider(Messages.get(this, "brightness"),
|
OptionSlider brightness = new OptionSlider(Messages.get(this, "brightness"),
|
||||||
Messages.get(this, "dark"), Messages.get(this, "bright"), -2, 2) {
|
Messages.get(this, "dark"), Messages.get(this, "bright"), -2, 2) {
|
||||||
|
@ -142,7 +139,7 @@ public class WndSettings extends WndTabbed {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
brightness.setSelectedValue(ShatteredPixelDungeon.brightness());
|
brightness.setSelectedValue(ShatteredPixelDungeon.brightness());
|
||||||
brightness.setRect(0, scale.bottom() + GAP_SML, WIDTH, SLIDER_HEIGHT);
|
brightness.setRect(0, scale.bottom() + GAP_TINY, WIDTH, SLIDER_HEIGHT);
|
||||||
add(brightness);
|
add(brightness);
|
||||||
|
|
||||||
CheckBox chkImmersive = new CheckBox( Messages.get(this, "soft_keys") ) {
|
CheckBox chkImmersive = new CheckBox( Messages.get(this, "soft_keys") ) {
|
||||||
|
@ -152,11 +149,34 @@ public class WndSettings extends WndTabbed {
|
||||||
ShatteredPixelDungeon.immerse(checked());
|
ShatteredPixelDungeon.immerse(checked());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
chkImmersive.setRect( 0, brightness.bottom() + GAP_LRG, WIDTH, BTN_HEIGHT );
|
chkImmersive.setRect( 0, brightness.bottom() + GAP_SML, WIDTH, BTN_HEIGHT );
|
||||||
chkImmersive.checked(ShatteredPixelDungeon.immersed());
|
chkImmersive.checked(ShatteredPixelDungeon.immersed());
|
||||||
chkImmersive.enable(android.os.Build.VERSION.SDK_INT >= 19);
|
chkImmersive.enable(android.os.Build.VERSION.SDK_INT >= 19);
|
||||||
add(chkImmersive);
|
add(chkImmersive);
|
||||||
|
|
||||||
|
CheckBox chkSaver = new CheckBox( Messages.get(this, "saver") ) {
|
||||||
|
@Override
|
||||||
|
protected void onClick() {
|
||||||
|
super.onClick();
|
||||||
|
checked( !checked() );
|
||||||
|
ShatteredPixelDungeon.scene().add(new WndOptions(
|
||||||
|
Messages.get(ScreenTab.class, "saver"),
|
||||||
|
Messages.get(ScreenTab.class, "saver_desc"),
|
||||||
|
Messages.get(ScreenTab.class, "okay"),
|
||||||
|
Messages.get(ScreenTab.class, "cancel")){
|
||||||
|
@Override
|
||||||
|
protected void onSelect(int index) {
|
||||||
|
if (index == 0){
|
||||||
|
checked( !checked() );
|
||||||
|
ShatteredPixelDungeon.powerSaver(checked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chkSaver.setRect( 0, chkImmersive.bottom() + GAP_TINY, WIDTH, BTN_HEIGHT );
|
||||||
|
chkSaver.checked(ShatteredPixelDungeon.powerSaver());
|
||||||
|
if (PixelScene.maxScreenZoom >= 2) add(chkSaver);
|
||||||
|
|
||||||
RedButton btnOrientation = new RedButton( ShatteredPixelDungeon.landscape() ?
|
RedButton btnOrientation = new RedButton( ShatteredPixelDungeon.landscape() ?
|
||||||
Messages.get(this, "portrait")
|
Messages.get(this, "portrait")
|
||||||
|
@ -166,7 +186,7 @@ public class WndSettings extends WndTabbed {
|
||||||
ShatteredPixelDungeon.landscape(!ShatteredPixelDungeon.landscape());
|
ShatteredPixelDungeon.landscape(!ShatteredPixelDungeon.landscape());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
btnOrientation.setRect(0, chkImmersive.bottom() + GAP_LRG, WIDTH, BTN_HEIGHT);
|
btnOrientation.setRect(0, chkSaver.bottom() + GAP_SML, WIDTH, BTN_HEIGHT);
|
||||||
add( btnOrientation );
|
add( btnOrientation );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,10 @@ windows.wndsettings$screentab.brightness=Brightness
|
||||||
windows.wndsettings$screentab.dark=Dark
|
windows.wndsettings$screentab.dark=Dark
|
||||||
windows.wndsettings$screentab.bright=Bright
|
windows.wndsettings$screentab.bright=Bright
|
||||||
windows.wndsettings$screentab.soft_keys=Hide Software Keys
|
windows.wndsettings$screentab.soft_keys=Hide Software Keys
|
||||||
|
windows.wndsettings$screentab.saver=Power Saver Mode
|
||||||
|
windows.wndsettings$screentab.saver_desc=Power saver mode draws the game at a reduced size and scales it up to fit your screen.\n\nThis will make the graphics less crisp, but will improve performance and battery life.\n\nYou may need to restart the game for changes to take effect.
|
||||||
|
windows.wndsettings$screentab.okay=Okay
|
||||||
|
windows.wndsettings$screentab.cancel=Cancel
|
||||||
windows.wndsettings$screentab.portrait=Switch to portrait
|
windows.wndsettings$screentab.portrait=Switch to portrait
|
||||||
windows.wndsettings$screentab.landscape=Switch to landscape
|
windows.wndsettings$screentab.landscape=Switch to landscape
|
||||||
windows.wndsettings$uitab.mode=Toolbar Mode:
|
windows.wndsettings$uitab.mode=Toolbar Mode:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user