mirror of
https://github.com/nextcloud/android.git
synced 2024-11-26 23:28:42 +03:00
Themes header: plain background color and logo, if available
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
parent
fa8d42a252
commit
fd9e0e2f69
10 changed files with 355 additions and 8 deletions
|
@ -2041,6 +2041,8 @@ public class FileDataStorageManager {
|
|||
capability.getServerBackground());
|
||||
contentValues.put(ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN,
|
||||
capability.getServerSlogan());
|
||||
contentValues.put(ProviderTableMeta.CAPABILITIES_SERVER_LOGO,
|
||||
capability.getServerLogo());
|
||||
contentValues.put(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION,
|
||||
capability.getEndToEndEncryption().getValue());
|
||||
contentValues.put(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT,
|
||||
|
@ -2185,6 +2187,7 @@ public class FileDataStorageManager {
|
|||
capability.setServerElementColor(getString(cursor, ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR));
|
||||
capability.setServerBackground(getString(cursor, ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL));
|
||||
capability.setServerSlogan(getString(cursor, ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN));
|
||||
capability.setServerLogo(getString(cursor, ProviderTableMeta.CAPABILITIES_SERVER_LOGO));
|
||||
capability.setEndToEndEncryption(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION));
|
||||
capability.setServerBackgroundDefault(
|
||||
getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT));
|
||||
|
|
|
@ -35,7 +35,7 @@ import java.util.List;
|
|||
*/
|
||||
public class ProviderMeta {
|
||||
public static final String DB_NAME = "filelist";
|
||||
public static final int DB_VERSION = 61;
|
||||
public static final int DB_VERSION = 62;
|
||||
|
||||
private ProviderMeta() {
|
||||
// No instance
|
||||
|
@ -211,6 +211,7 @@ public class ProviderMeta {
|
|||
public static final String CAPABILITIES_SERVER_ELEMENT_COLOR = "server_element_color";
|
||||
public static final String CAPABILITIES_SERVER_BACKGROUND_URL = "background_url";
|
||||
public static final String CAPABILITIES_SERVER_SLOGAN = "server_slogan";
|
||||
public static final String CAPABILITIES_SERVER_LOGO = "server_logo";
|
||||
public static final String CAPABILITIES_SERVER_BACKGROUND_DEFAULT = "background_default";
|
||||
public static final String CAPABILITIES_SERVER_BACKGROUND_PLAIN = "background_plain";
|
||||
public static final String CAPABILITIES_END_TO_END_ENCRYPTION = "end_to_end_encryption";
|
||||
|
|
|
@ -783,6 +783,7 @@ public class FileContentProvider extends ContentProvider {
|
|||
+ ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR + TEXT
|
||||
+ ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR + TEXT
|
||||
+ ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN + TEXT
|
||||
+ ProviderTableMeta.CAPABILITIES_SERVER_LOGO + TEXT
|
||||
+ ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL + TEXT
|
||||
+ ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION + INTEGER
|
||||
+ ProviderTableMeta.CAPABILITIES_ACTIVITY + INTEGER
|
||||
|
@ -2305,6 +2306,27 @@ public class FileContentProvider extends ContentProvider {
|
|||
if (!upgraded) {
|
||||
Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
|
||||
if (oldVersion < 62 && newVersion >= 62) {
|
||||
Log_OC.i(SQL, "Entering in the #62 add logo to capability");
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL(ALTER_TABLE + ProviderTableMeta.CAPABILITIES_TABLE_NAME +
|
||||
ADD_COLUMN + ProviderTableMeta.CAPABILITIES_SERVER_LOGO + " TEXT ");
|
||||
|
||||
// force refresh
|
||||
db.execSQL("UPDATE capabilities SET etag = '' WHERE 1=1");
|
||||
|
||||
upgraded = true;
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,21 +33,34 @@ import android.app.Activity;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.ViewGroup.MarginLayoutParams;
|
||||
import android.webkit.URLUtil;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.GenericRequestBuilder;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.StreamEncoder;
|
||||
import com.bumptech.glide.load.resource.file.FileToStreamDecoder;
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
@ -93,6 +106,9 @@ import com.owncloud.android.utils.DisplayUtils;
|
|||
import com.owncloud.android.utils.DrawerMenuUtil;
|
||||
import com.owncloud.android.utils.FilesSyncHelper;
|
||||
import com.owncloud.android.utils.svg.MenuSimpleTarget;
|
||||
import com.owncloud.android.utils.svg.SVGorImage;
|
||||
import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder;
|
||||
import com.owncloud.android.utils.svg.SvgOrImageDecoder;
|
||||
import com.owncloud.android.utils.theme.ThemeBarUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeColorUtils;
|
||||
import com.owncloud.android.utils.theme.ThemeDrawableUtils;
|
||||
|
@ -104,6 +120,7 @@ import org.greenrobot.eventbus.ThreadMode;
|
|||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -212,8 +229,7 @@ public abstract class DrawerActivity extends ToolbarActivity
|
|||
|
||||
// Setting up drawer header
|
||||
mNavigationViewHeader = mNavigationView.getHeaderView(0);
|
||||
FrameLayout drawerHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_view);
|
||||
setupDrawerHeader(drawerHeader);
|
||||
updateHeader();
|
||||
|
||||
setupDrawerMenu(mNavigationView);
|
||||
getAndDisplayUserQuota();
|
||||
|
@ -278,6 +294,73 @@ public abstract class DrawerActivity extends ToolbarActivity
|
|||
ThemeBarUtils.colorProgressBar(mQuotaProgressBar, ThemeColorUtils.primaryColor(this));
|
||||
}
|
||||
|
||||
public void updateHeader() {
|
||||
if (getAccount() != null &&
|
||||
getStorageManager().getCapability(getAccount().name).getServerBackground() != null) {
|
||||
|
||||
OCCapability capability = getStorageManager().getCapability(getAccount().name);
|
||||
String logo = capability.getServerLogo();
|
||||
int primaryColor = ThemeColorUtils.primaryColor(getAccount(), false, this);
|
||||
|
||||
// set background to primary color
|
||||
LinearLayout drawerHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_view);
|
||||
drawerHeader.setBackgroundColor(ThemeColorUtils.unchangedPrimaryColor(getAccount(), this));
|
||||
|
||||
if (!TextUtils.isEmpty(logo) && URLUtil.isValidUrl(logo)) {
|
||||
// background image
|
||||
GenericRequestBuilder<Uri, InputStream, SVGorImage, Bitmap> requestBuilder = Glide.with(this)
|
||||
.using(Glide.buildStreamModelLoader(Uri.class, this), InputStream.class)
|
||||
.from(Uri.class)
|
||||
.as(SVGorImage.class)
|
||||
.transcode(new SvgOrImageBitmapTranscoder(128, 128), Bitmap.class)
|
||||
.sourceEncoder(new StreamEncoder())
|
||||
.cacheDecoder(new FileToStreamDecoder<>(new SvgOrImageDecoder()))
|
||||
.decoder(new SvgOrImageDecoder());
|
||||
|
||||
// background image
|
||||
SimpleTarget target = new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) {
|
||||
Drawable[] drawables = {new ColorDrawable(primaryColor), new BitmapDrawable(resource)};
|
||||
LayerDrawable layerDrawable = new LayerDrawable(drawables);
|
||||
|
||||
String name = capability.getServerName();
|
||||
setDrawerHeaderLogo(layerDrawable, name);
|
||||
}
|
||||
};
|
||||
|
||||
requestBuilder
|
||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
||||
.load(Uri.parse(logo))
|
||||
.into(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setDrawerHeaderLogo(Drawable drawable, String name) {
|
||||
ImageView imageHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_logo);
|
||||
imageHeader.setImageDrawable(drawable);
|
||||
imageHeader.setScaleType(ImageView.ScaleType.FIT_START);
|
||||
imageHeader.setAdjustViewBounds(true);
|
||||
|
||||
imageHeader.setMaxWidth(DisplayUtils.convertDpToPixel(100f, this));
|
||||
|
||||
MarginLayoutParams oldParam = (MarginLayoutParams) imageHeader.getLayoutParams();
|
||||
MarginLayoutParams params = new MarginLayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.MATCH_PARENT);
|
||||
params.leftMargin = oldParam.leftMargin;
|
||||
params.rightMargin = oldParam.rightMargin;
|
||||
|
||||
imageHeader.setLayoutParams(new LinearLayout.LayoutParams(params));
|
||||
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
TextView serverName = mNavigationViewHeader.findViewById(R.id.drawer_header_server_name);
|
||||
serverName.setText(name);
|
||||
serverName.setTextColor(ThemeColorUtils.unchangedFontColor(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* setup drawer header, basically the logo color
|
||||
*/
|
||||
|
|
0
src/main/java/com/owncloud/android/utils/ThemeUtils.java
Normal file
0
src/main/java/com/owncloud/android/utils/ThemeUtils.java
Normal file
47
src/main/java/com/owncloud/android/utils/svg/SVGorImage.java
Normal file
47
src/main/java/com/owncloud/android/utils/svg/SVGorImage.java
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* Adapted from https://stackoverflow.com/a/54523482
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.owncloud.android.utils.svg;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
public class SVGorImage {
|
||||
private SVG svg;
|
||||
private Bitmap bitmap;
|
||||
|
||||
public SVGorImage(SVG svg, Bitmap bitmap) {
|
||||
this.svg = svg;
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
|
||||
public SVG getSVG() {
|
||||
return svg;
|
||||
}
|
||||
|
||||
public Bitmap getBitmap() {
|
||||
return bitmap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Tobias Kaminsky
|
||||
* Copyright (C) 2021 Tobias Kaminsky
|
||||
* Copyright (C) 2021 Nextcloud GmbH
|
||||
*
|
||||
* Adapted from https://stackoverflow.com/a/54523482
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.owncloud.android.utils.svg;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||
|
||||
/**
|
||||
* Convert the {@link SVG}'s internal representation to a Bitmap.
|
||||
*/
|
||||
public class SvgOrImageBitmapTranscoder implements ResourceTranscoder<SVGorImage, Bitmap> {
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public SvgOrImageBitmapTranscoder(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource<Bitmap> transcode(Resource<SVGorImage> toTranscode) {
|
||||
SVGorImage svGorImage = toTranscode.get();
|
||||
|
||||
if (svGorImage.getSVG() != null) {
|
||||
SVG svg = svGorImage.getSVG();
|
||||
|
||||
try {
|
||||
svg.setDocumentHeight("100%");
|
||||
svg.setDocumentWidth("100%");
|
||||
} catch (SVGParseException e) {
|
||||
Log_OC.e(this, "Could not set document size. Output might have wrong size");
|
||||
}
|
||||
|
||||
// Create a canvas to draw onto
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
// Render our document onto our canvas
|
||||
svg.renderToCanvas(canvas);
|
||||
|
||||
return new SimpleResource<>(bitmap);
|
||||
} else {
|
||||
Bitmap bitmap = svGorImage.getBitmap();
|
||||
|
||||
return new SimpleResource<>(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* Copyright 2014 Google, Inc. All rights reserved.
|
||||
* Licenced under the BSD licence
|
||||
*
|
||||
* Borrowed from:
|
||||
* https://github.com/bumptech/glide/blob/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg/
|
||||
* SvgDecoder.java
|
||||
*
|
||||
* Adapted from https://stackoverflow.com/a/54523482
|
||||
*
|
||||
*/
|
||||
|
||||
package com.owncloud.android.utils.svg;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.caverock.androidsvg.PreserveAspectRatio;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Decodes an SVG internal representation from an {@link InputStream}.
|
||||
*/
|
||||
public class SvgOrImageDecoder implements ResourceDecoder<InputStream, SVGorImage> {
|
||||
private int height = -1;
|
||||
private int width = -1;
|
||||
|
||||
public SvgOrImageDecoder(int height, int width) {
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public SvgOrImageDecoder() {
|
||||
// empty constructor
|
||||
}
|
||||
|
||||
public Resource<SVGorImage> decode(InputStream source, int w, int h) throws IOException {
|
||||
byte[] array = new byte[source.available()];
|
||||
source.read(array);
|
||||
ByteArrayInputStream svgInputStream = new ByteArrayInputStream(array.clone());
|
||||
ByteArrayInputStream pngInputStream = new ByteArrayInputStream(array.clone());
|
||||
|
||||
try {
|
||||
SVG svg = SVG.getFromInputStream(svgInputStream);
|
||||
source.close();
|
||||
pngInputStream.close();
|
||||
|
||||
if (width > 0) {
|
||||
svg.setDocumentWidth(width);
|
||||
}
|
||||
if (height > 0) {
|
||||
svg.setDocumentHeight(height);
|
||||
}
|
||||
svg.setDocumentPreserveAspectRatio(PreserveAspectRatio.LETTERBOX);
|
||||
|
||||
return new SimpleResource<>(new SVGorImage(svg, null));
|
||||
} catch (SVGParseException ex) {
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(pngInputStream);
|
||||
return new SimpleResource<>(new SVGorImage(null, bitmap));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "SvgDecoder.com.owncloud.android";
|
||||
}
|
||||
}
|
|
@ -316,4 +316,24 @@ public final class ThemeColorUtils {
|
|||
public static String primaryColorToHexString(Context context) {
|
||||
return String.format("#%06X", 0xFFFFFF & primaryColor(context, true));
|
||||
}
|
||||
|
||||
public static int unchangedPrimaryColor(Account account, Context context) {
|
||||
try {
|
||||
return Color.parseColor(getCapability(account, context).getServerColor());
|
||||
} catch (Exception e) {
|
||||
return context.getResources().getColor(R.color.primary);
|
||||
}
|
||||
}
|
||||
|
||||
public static int unchangedFontColor(Context context) {
|
||||
try {
|
||||
return Color.parseColor(getCapability(context).getServerTextColor());
|
||||
} catch (Exception e) {
|
||||
if (darkTheme(context)) {
|
||||
return Color.WHITE;
|
||||
} else {
|
||||
return Color.BLACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Nextcloud Android client application
|
||||
|
||||
Copyright (C) 2016 Andy Scherzinger
|
||||
|
@ -18,19 +19,31 @@
|
|||
You should have received a copy of the GNU Affero General Public
|
||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_header_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/drawer_header_height"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="horizontal"
|
||||
tools:background="@color/primary">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/drawer_header_logo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:contentDescription="@string/empty"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/nextcloud_logo" />
|
||||
|
||||
</FrameLayout>
|
||||
<TextView
|
||||
android:id="@+id/drawer_header_server_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:ellipsize="middle"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Custom Server Name" />
|
||||
</LinearLayout>
|
||||
|
|
Loading…
Reference in a new issue