diff --git a/Clover/app/src/main/java/org/floens/chan/core/ChanPreferences.java b/Clover/app/src/main/java/org/floens/chan/core/ChanPreferences.java index 33645d9d..114c389c 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/ChanPreferences.java +++ b/Clover/app/src/main/java/org/floens/chan/core/ChanPreferences.java @@ -17,28 +17,36 @@ */ package org.floens.chan.core; +import android.content.SharedPreferences; + import org.floens.chan.ChanApplication; +import org.floens.chan.R; import org.floens.chan.service.WatchService; +import org.floens.chan.utils.ThemeHelper; public class ChanPreferences { + private static SharedPreferences p() { + return ChanApplication.getPreferences(); + } + public static boolean getOpenLinkConfirmation() { - return ChanApplication.getPreferences().getBoolean("preference_open_link_confirmation", true); + return p().getBoolean("preference_open_link_confirmation", true); } public static String getDefaultName() { - return ChanApplication.getPreferences().getString("preference_default_name", ""); + return p().getString("preference_default_name", ""); } public static String getDefaultEmail() { - return ChanApplication.getPreferences().getString("preference_default_email", ""); + return p().getString("preference_default_email", ""); } public static boolean getDeveloper() { - return ChanApplication.getPreferences().getBoolean("preference_developer", false); + return p().getBoolean("preference_developer", false); } public static void setDeveloper(boolean developer) { - ChanApplication.getPreferences().edit().putBoolean("preference_developer", developer).commit(); + p().edit().putBoolean("preference_developer", developer).commit(); } public static String getImageSaveDirectory() { @@ -46,7 +54,7 @@ public class ChanPreferences { } public static boolean getWatchEnabled() { - return ChanApplication.getPreferences().getBoolean("preference_watch_enabled", false); + return p().getBoolean("preference_watch_enabled", false); } /** @@ -57,48 +65,52 @@ public class ChanPreferences { */ public static void setWatchEnabled(boolean enabled) { if (getWatchEnabled() != enabled) { - ChanApplication.getPreferences().edit().putBoolean("preference_watch_enabled", enabled).commit(); + p().edit().putBoolean("preference_watch_enabled", enabled).commit(); WatchService.updateRunningState(ChanApplication.getInstance()); ChanApplication.getPinnedManager().onPinsChanged(); } } public static boolean getWatchBackgroundEnabled() { - return ChanApplication.getPreferences().getBoolean("preference_watch_background_enabled", true); + return p().getBoolean("preference_watch_background_enabled", true); } public static long getWatchBackgroundTimeout() { - String number = ChanApplication.getPreferences().getString("preference_watch_background_timeout", "0"); + String number = p().getString("preference_watch_background_timeout", "0"); return Integer.parseInt(number) * 1000L; } public static boolean getVideoAutoPlay() { - return ChanApplication.getPreferences().getBoolean("preference_autoplay", false); + return p().getBoolean("preference_autoplay", false); } public static boolean getPassEnabled() { - return ChanApplication.getPreferences().getBoolean("preference_pass_enabled", false); + return p().getBoolean("preference_pass_enabled", false); } public static void setPassEnabled(boolean enabled) { if (getPassEnabled() != enabled) { - ChanApplication.getPreferences().edit().putBoolean("preference_pass_enabled", enabled).commit(); + p().edit().putBoolean("preference_pass_enabled", enabled).commit(); } } public static String getPassToken() { - return ChanApplication.getPreferences().getString("preference_pass_token", ""); + return p().getString("preference_pass_token", ""); } public static String getPassPin() { - return ChanApplication.getPreferences().getString("preference_pass_pin", ""); + return p().getString("preference_pass_pin", ""); } public static void setPassId(String id) { - ChanApplication.getPreferences().edit().putString("preference_pass_id", id).commit(); + p().edit().putString("preference_pass_id", id).commit(); } public static String getPassId() { - return ChanApplication.getPreferences().getString("preference_pass_id", ""); + return p().getString("preference_pass_id", ""); + } + + public static String getTheme() { + return p().getString("preference_theme", "light"); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/AboutActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/AboutActivity.java index 1e564f5d..507f1ede 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/AboutActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/AboutActivity.java @@ -21,11 +21,15 @@ import android.app.Activity; import android.os.Bundle; import android.webkit.WebView; +import org.floens.chan.utils.ThemeHelper; + public class AboutActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + WebView webView = new WebView(this); webView.loadUrl("file:///android_asset/html/licences.html"); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java index eedc2023..bc367d95 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/BaseActivity.java @@ -21,6 +21,7 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.net.Uri; import android.nfc.NdefMessage; @@ -52,6 +53,7 @@ import org.floens.chan.ui.BadgeDrawable; import org.floens.chan.ui.SwipeDismissListViewTouchListener; import org.floens.chan.ui.SwipeDismissListViewTouchListener.DismissCallbacks; import org.floens.chan.ui.adapter.PinnedAdapter; +import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Utils; import java.util.List; @@ -87,6 +89,8 @@ public abstract class BaseActivity extends Activity implements PanelSlideListene protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + setContentView(R.layout.activity_base); pinDrawer = (DrawerLayout) findViewById(R.id.drawer_layout); @@ -111,7 +115,12 @@ public abstract class BaseActivity extends Activity implements PanelSlideListene threadPane.setPanelSlideListener(this); threadPane.setParallaxDistance(200); threadPane.setShadowResource(R.drawable.panel_shadow); - threadPane.setSliderFadeColor(0xcce5e5e5); + + TypedArray ta = obtainStyledAttributes(null, R.styleable.BoardPane, R.attr.board_pane_style, 0); + int color = ta.getColor(R.styleable.BoardPane_fade_color, 0); + ta.recycle(); + + threadPane.setSliderFadeColor(color); threadPane.openPane(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java index 13b35eb7..39d9d585 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/BoardEditor.java @@ -49,6 +49,7 @@ import org.floens.chan.R; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.Board; import org.floens.chan.ui.SwipeDismissListViewTouchListener; +import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Utils; import java.util.ArrayList; @@ -66,6 +67,8 @@ public class BoardEditor extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + list = boardManager.getSavedBoards(); adapter = new BoardEditAdapter(this, 0, list); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/DeveloperActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/DeveloperActivity.java index 10f84e3c..df96cefc 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/DeveloperActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/DeveloperActivity.java @@ -25,12 +25,15 @@ import android.widget.LinearLayout; import android.widget.TextView; import org.floens.chan.ChanApplication; +import org.floens.chan.utils.ThemeHelper; public class DeveloperActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + LinearLayout wrapper = new LinearLayout(this); wrapper.setOrientation(LinearLayout.VERTICAL); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java index 0c55367d..5063077c 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java @@ -33,6 +33,7 @@ import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.fragment.ImageViewFragment; import org.floens.chan.utils.ImageSaver; import org.floens.chan.utils.Logger; +import org.floens.chan.utils.ThemeHelper; import java.util.ArrayList; import java.util.List; @@ -70,6 +71,8 @@ public class ImageViewActivity extends Activity implements ViewPager.OnPageChang super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + if (postAdapter != null) { // Get the posts with images ArrayList imagePosts = new ArrayList(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java index ec3d7647..92404736 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java @@ -41,6 +41,7 @@ import org.floens.chan.core.ChanPreferences; import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager.PassResponse; import org.floens.chan.core.model.Pass; +import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Utils; public class PassSettingsActivity extends Activity implements OnCheckedChangeListener { @@ -52,6 +53,8 @@ public class PassSettingsActivity extends Activity implements OnCheckedChangeLis protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + instance = this; setFragment(ChanPreferences.getPassEnabled()); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ReplyActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/ReplyActivity.java index ee5ad2c2..14871ce7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ReplyActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/ReplyActivity.java @@ -25,6 +25,7 @@ import android.view.MenuItem; import org.floens.chan.core.model.Loadable; import org.floens.chan.ui.fragment.ReplyFragment; import org.floens.chan.utils.Logger; +import org.floens.chan.utils.ThemeHelper; public class ReplyActivity extends Activity { private static final String TAG = "ReplyActivity"; @@ -39,6 +40,8 @@ public class ReplyActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + if (loadable != null) { getActionBar().setDisplayHomeAsUpEnabled(true); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/SettingsActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/SettingsActivity.java index d9957b85..2bd13673 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/SettingsActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/SettingsActivity.java @@ -18,15 +18,55 @@ package org.floens.chan.ui.activity; import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import org.floens.chan.ui.fragment.SettingsFragment; +import org.floens.chan.utils.Logger; +import org.floens.chan.utils.ThemeHelper; public class SettingsActivity extends Activity { + private static boolean doingThemeRestart = false; + private static ThemeHelper.Theme lastTheme; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getFragmentManager().beginTransaction().replace(android.R.id.content, new SettingsFragment()).commit(); + if (!doingThemeRestart) { + lastTheme = ThemeHelper.getTheme(); + } + + ThemeHelper.setTheme(this); + + SettingsFragment frag = new SettingsFragment(); + frag.setArguments(getIntent().getExtras()); + getFragmentManager().beginTransaction().replace(android.R.id.content, frag).commit(); + } + + public void restart(Intent intent) { + doingThemeRestart = true; + startActivity(intent); + finish(); + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (doingThemeRestart) { + doingThemeRestart = false; + } else { + if (ThemeHelper.getTheme() != lastTheme) { + lastTheme = ThemeHelper.getTheme(); + Logger.test("THEME CHANGED!"); + + Intent intent = new Intent(this, BoardActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + startActivity(intent); + } + } } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/WatchSettingsActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/WatchSettingsActivity.java index c71d5e4f..cb10f9f4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/WatchSettingsActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/WatchSettingsActivity.java @@ -40,6 +40,7 @@ import android.widget.TextView; import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; +import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Utils; public class WatchSettingsActivity extends Activity implements OnCheckedChangeListener { @@ -49,6 +50,8 @@ public class WatchSettingsActivity extends Activity implements OnCheckedChangeLi protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ThemeHelper.setTheme(this); + setFragment(ChanPreferences.getWatchEnabled()); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/SettingsFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/SettingsFragment.java index 28d8c39d..535d0275 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/SettingsFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/SettingsFragment.java @@ -20,15 +20,20 @@ package org.floens.chan.ui.fragment; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceFragment; import android.preference.PreferenceGroup; +import android.view.View; +import android.widget.ListView; import android.widget.Toast; import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; import org.floens.chan.ui.activity.AboutActivity; +import org.floens.chan.ui.activity.SettingsActivity; +import org.floens.chan.utils.ThemeHelper; public class SettingsFragment extends PreferenceFragment { private int clickCount = 0; @@ -87,6 +92,43 @@ public class SettingsFragment extends PreferenceFragment { developerPreference = findPreference("about_developer"); ((PreferenceGroup) findPreference("group_about")).removePreference(developerPreference); updateDeveloperPreference(); + + final ListPreference theme = (ListPreference) findPreference("preference_theme"); + String currentValue = theme.getValue(); + if (currentValue == null) { + theme.setValue((String) theme.getEntryValues()[0]); + currentValue = theme.getValue(); + } + updateThemeSummary(theme, currentValue.toString()); + + theme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + updateThemeSummary(theme, newValue.toString()); + + // Thanks! https://github.com/CyanogenMod/android_packages_apps_Calculator/blob/cm-10.2/src/com/android/calculator2/view/PreferencesFragment.java + if (!newValue.toString().equals(ThemeHelper.getTheme().name)) { + Intent intent = new Intent(getActivity(), SettingsActivity.class); + + intent.putExtra("pos", getListView().getFirstVisiblePosition()); + View child = getListView().getChildAt(0); + intent.putExtra("off", child != null ? child.getTop() : 0); + + ((SettingsActivity) getActivity()).restart(intent); + } + + return true; + } + }); + } + + public void onStart() { + super.onStart(); + + final Bundle args = getArguments(); + if (args != null) { + getListView().setSelectionFromTop(args.getInt("pos", 0), args.getInt("off", 0)); + } } @Override @@ -106,6 +148,10 @@ public class SettingsFragment extends PreferenceFragment { } } + private ListView getListView() { + return (ListView) getView().findViewById(android.R.id.list); + } + private void updateDeveloperPreference() { if (ChanPreferences.getDeveloper()) { ((PreferenceGroup) findPreference("group_about")).addPreference(developerPreference); @@ -113,4 +159,9 @@ public class SettingsFragment extends PreferenceFragment { ((PreferenceGroup) findPreference("group_about")).removePreference(developerPreference); } } + + private void updateThemeSummary(ListPreference list, String value) { + int index = list.findIndexOfValue(value); + list.setSummary(list.getEntries()[index]); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java index b11d1b8b..00abdef7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java @@ -20,6 +20,7 @@ package org.floens.chan.ui.view; import android.app.Activity; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Color; import android.text.Layout; import android.text.Selection; @@ -78,6 +79,11 @@ public class PostView extends LinearLayout implements View.OnClickListener, View private NetworkImageView countryView; private View lastSeen; + private int thumbnailBackground; + private int savedReplyColor; + private int highlightedColor; + private int replyCountColor; + /** * Represents a post. Use setPost(Post ThreadManager) to fill it with data. * setPost can be called multiple times (useful for ListView). @@ -87,16 +93,19 @@ public class PostView extends LinearLayout implements View.OnClickListener, View public PostView(Context activity) { super(activity); context = (Activity) activity; + init(); } public PostView(Context activity, AttributeSet attbs) { super(activity, attbs); context = (Activity) activity; + init(); } public PostView(Context activity, AttributeSet attbs, int style) { super(activity, attbs, style); context = (Activity) activity; + init(); } @Override @@ -228,9 +237,9 @@ public class PostView extends LinearLayout implements View.OnClickListener, View } if (post.isSavedReply) { - full.setBackgroundColor(0xFFBCBCBC); + full.setBackgroundColor(savedReplyColor); } else if (manager.isPostHightlighted(post)) { - full.setBackgroundColor(0xFFD6BAD0); + full.setBackgroundColor(highlightedColor); } else { full.setBackgroundColor(0x00000000); } @@ -246,6 +255,15 @@ public class PostView extends LinearLayout implements View.OnClickListener, View } } + private void init() { + TypedArray ta = context.obtainStyledAttributes(null, R.styleable.PostView, R.attr.post_style, 0); + thumbnailBackground = ta.getColor(R.styleable.PostView_thumbnail_background, 0); + savedReplyColor = ta.getColor(R.styleable.PostView_saved_reply_color, 0); + highlightedColor = ta.getColor(R.styleable.PostView_highlighted_color, 0); + replyCountColor = ta.getColor(R.styleable.PostView_reply_count_color, 0); + ta.recycle(); + } + private void buildView(final Context context) { if (isBuild) return; @@ -277,7 +295,7 @@ public class PostView extends LinearLayout implements View.OnClickListener, View LinearLayout left = new LinearLayout(context); left.setOrientation(VERTICAL); - left.setBackgroundColor(0xffdddddd); + left.setBackgroundColor(thumbnailBackground); left.addView(imageView, new LinearLayout.LayoutParams(imageSize, imageSize)); @@ -326,7 +344,7 @@ public class PostView extends LinearLayout implements View.OnClickListener, View // This behavior differs with 4.4 / 4.1 Utils.setPressedDrawable(repliesCountView); - repliesCountView.setTextColor(Color.argb(255, 100, 100, 100)); + repliesCountView.setTextColor(replyCountColor); repliesCountView.setPadding(postPadding, postPadding, postPadding, postPadding); repliesCountView.setTextSize(14); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ThemeHelper.java b/Clover/app/src/main/java/org/floens/chan/utils/ThemeHelper.java new file mode 100644 index 00000000..f8155309 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/utils/ThemeHelper.java @@ -0,0 +1,41 @@ +package org.floens.chan.utils; + +import android.app.Activity; + +import org.floens.chan.R; +import org.floens.chan.core.ChanPreferences; + +public class ThemeHelper { + public enum Theme { + LIGHT("light", R.style.AppTheme), + DARK("dark", R.style.AppTheme_Dark), + BLACK("black", R.style.AppTheme_Dark); + + public String name; + public int resValue; + + private Theme(String name, int resValue) { + this.name = name; + this.resValue = resValue; + } + } + + public static void setTheme(Activity activity) { + activity.setTheme(getTheme().resValue); + } + + public static Theme getTheme() { + String themeName = ChanPreferences.getTheme(); + + Theme theme = null; + if (themeName.equals("light")) { + theme = Theme.LIGHT; + } else if (themeName.equals("dark")) { + theme = Theme.DARK; + } else if (themeName.equals("black")) { + theme = Theme.BLACK; + } + + return theme; + } +} diff --git a/Clover/app/src/main/res/layout/activity_base.xml b/Clover/app/src/main/res/layout/activity_base.xml index 059dacab..17470dd1 100644 --- a/Clover/app/src/main/res/layout/activity_base.xml +++ b/Clover/app/src/main/res/layout/activity_base.xml @@ -12,14 +12,14 @@ android:id="@+id/left_pane" android:layout_width="100dp" android:layout_height="match_parent" - android:background="#FFFFFFFF" /> + style="?board_pane_left_style" /> + style="?board_pane_right_style"/> - #88000000 - @@ -13,4 +11,21 @@ + + + + + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 4361bb01..d8760c0e 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -100,6 +100,19 @@ 4chan pass General + + Theme + + Light + Dark + Black + + + light + dark + black + + Ask before opening links Start playing videos directly diff --git a/Clover/app/src/main/res/values/styles.xml b/Clover/app/src/main/res/values/styles.xml index a2bd440f..7b4c3f2b 100644 --- a/Clover/app/src/main/res/values/styles.xml +++ b/Clover/app/src/main/res/values/styles.xml @@ -1,18 +1,68 @@ - - - + + + + + + #88000000 + - - - - + + + + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/xml/preference.xml b/Clover/app/src/main/res/xml/preference.xml index f16d17f6..001e8040 100644 --- a/Clover/app/src/main/res/xml/preference.xml +++ b/Clover/app/src/main/res/xml/preference.xml @@ -27,6 +27,14 @@ + +