diff --git a/Clover/app/src/main/assets/captcha/captcha.html b/Clover/app/src/main/assets/captcha/captcha.html index cad918fb..6f05f5c6 100644 --- a/Clover/app/src/main/assets/captcha/captcha.html +++ b/Clover/app/src/main/assets/captcha/captcha.html @@ -13,6 +13,7 @@ text-align: center; margin: 40px auto 0 auto; } + @@ -38,9 +39,11 @@ window.globalOnCaptchaLoaded = function() { window.onerror = function(message, url, line) { document.getElementById('captcha-loading').style.display = 'none'; - document.getElementById('captcha-error').appendChild(document.createTextNode(line + ': ' + message + ' @ ' + url)); + document.getElementById('captcha-error').appendChild(document.createTextNode( + 'Captcha error at ' + line + ': ' + message + ' @ ' + url)); } })(); + diff --git a/Clover/app/src/main/java/org/floens/chan/core/di/AppModule.java b/Clover/app/src/main/java/org/floens/chan/core/di/AppModule.java index 604077b9..9f76e928 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/di/AppModule.java +++ b/Clover/app/src/main/java/org/floens/chan/core/di/AppModule.java @@ -36,7 +36,6 @@ import org.floens.chan.ui.controller.FiltersController; import org.floens.chan.ui.controller.HistoryController; import org.floens.chan.ui.controller.ImageViewerController; import org.floens.chan.ui.controller.MainSettingsController; -import org.floens.chan.ui.controller.PassSettingsController; import org.floens.chan.ui.controller.SiteSetupController; import org.floens.chan.ui.controller.SitesSetupController; import org.floens.chan.ui.controller.ViewThreadController; @@ -80,7 +79,6 @@ import dagger.Provides; WatchNotifier.class, WatchUpdateReceiver.class, ImagePickDelegate.class, - PassSettingsController.class, FiltersController.class, PostsFilter.class, ChanLoader.class, diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 5bc93a6d..35657359 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -74,7 +74,6 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe private boolean moreOpen; private boolean previewOpen; private boolean pickingFile; - private boolean authenticationInited; private int selectedQuote = -1; @Inject @@ -114,10 +113,6 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe showPreview(draft.fileName, draft.file); } - if (authenticationInited) { - callback.resetAuthentication(); - } - switchPage(Page.INPUT, false); } @@ -204,7 +199,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe draft.spoilerImage = draft.spoilerImage && board.spoilers; draft.captchaResponse = null; - if (false) { + if (loadable.site.postRequiresAuthentication()) { switchPage(Page.AUTHENTICATION, true); } else { makeSubmitCall(); @@ -258,10 +253,6 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe callback.openMessage(true, false, getString(R.string.reply_error), true); } - @Override - public void onAuthenticationLoaded(AuthenticationLayoutInterface authenticationLayout) { - } - @Override public void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response) { draft.captchaChallenge = challenge; @@ -377,13 +368,9 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe callback.setPage(Page.INPUT, animate); break; case AUTHENTICATION: - if (!authenticationInited) { - authenticationInited = true; - - Authentication authentication = loadable.site.postAuthenticate(); - callback.initializeAuthentication(loadable.site, authentication, this); - } + Authentication authentication = loadable.site.postAuthenticate(); + callback.initializeAuthentication(loadable.site, authentication, this); callback.setPage(Page.AUTHENTICATION, true); break; diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java index 5f46cdf7..48a9c59d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java @@ -9,6 +9,7 @@ public class SiteSetupPresenter { private Callback callback; private Site site; private DatabaseManager databaseManager; + private boolean hasLogin; @Inject public SiteSetupPresenter(DatabaseManager databaseManager) { @@ -18,10 +19,19 @@ public class SiteSetupPresenter { public void create(Callback callback, Site site) { this.callback = callback; this.site = site; + + hasLogin = site.feature(Site.Feature.LOGIN); + + if (hasLogin) { + callback.showLogin(); + } } public void show() { setBoardCount(callback, site); + if (hasLogin) { + callback.setIsLoggedIn(site.isLoggedIn()); + } } private void setBoardCount(Callback callback, Site site) { @@ -34,5 +44,9 @@ public class SiteSetupPresenter { public interface Callback { void setBoardCount(int boardCount); + + void showLogin(); + + void setIsLoggedIn(boolean isLoggedIn); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java index 88f755cb..7e7e9682 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java @@ -173,11 +173,25 @@ public interface Site { void post(Reply reply, PostListener postListener); interface PostListener { + void onPostComplete(HttpCall httpCall, ReplyResponse replyResponse); void onPostError(HttpCall httpCall); + } + boolean postRequiresAuthentication(); + + /** + * If {@link ReplyResponse#requireAuthentication} was {@code true}, or if + * {@link #postRequiresAuthentication()} is {@code true}, get the authentication + * required to post. + *

+ *

Some sites know beforehand if you need to authenticate, some sites only report it + * after posting. That's why there are two methods.

+ * + * @return an {@link Authentication} model that describes the way to authenticate. + */ Authentication postAuthenticate(); void delete(DeleteRequest deleteRequest, DeleteListener deleteListener); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java index 6ba78f87..5477ef27 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java @@ -82,4 +82,9 @@ public abstract class SiteBase implements Site { boardManager.createAll(Collections.singletonList(board)); return board; } + + @Override + public boolean postRequiresAuthentication() { + return false; + } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java index 4b810731..da990118 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java @@ -373,6 +373,11 @@ public class Chan4 extends SiteBase { }); } + @Override + public boolean postRequiresAuthentication() { + return !isLoggedIn(); + } + @Override public Authentication postAuthenticate() { if (isLoggedIn()) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java index 43917399..ab77eb22 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java @@ -93,8 +93,8 @@ public class Chan8ReplyHttpCall extends CommonReplyHttpCall { } else if (errorMessageMatcher.find()) { replyResponse.errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text(); } else { - // TODO: 8ch redirects us, but the result is a 404. - // stop redirecting. + // TODO(multisite): 8ch redirects us, but the result is a 404, and we need that + // redirect url to figure out what we posted. HttpUrl url = response.request().url(); List segments = url.pathSegments(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java index cb056c95..019f85f8 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java @@ -18,8 +18,6 @@ package org.floens.chan.ui.captcha; public interface AuthenticationLayoutCallback { - void onAuthenticationLoaded(AuthenticationLayoutInterface authenticationLayout); - void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java index abb9714f..2ae092b8 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java @@ -70,6 +70,8 @@ public class CaptchaLayout extends WebView implements AuthenticationLayoutInterf this.siteKey = authentication.siteKey; this.baseUrl = authentication.baseUrl; + requestDisallowInterceptTouchEvent(true); + AndroidUtils.hideKeyboard(this); WebSettings settings = getSettings(); @@ -119,7 +121,6 @@ public class CaptchaLayout extends WebView implements AuthenticationLayoutInterf } private void onCaptchaLoaded() { - callback.onAuthenticationLoaded(this); } private void onCaptchaEntered(String challenge, String response) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java similarity index 86% rename from Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java rename to Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java index b2644337..3c808ae6 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java @@ -28,28 +28,16 @@ import android.widget.TextView; import org.floens.chan.R; import org.floens.chan.controller.Controller; -import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.site.Site; -import org.floens.chan.core.site.Sites; import org.floens.chan.core.site.http.HttpCall; -import org.floens.chan.core.site.http.HttpCallManager; import org.floens.chan.core.site.http.LoginRequest; import org.floens.chan.core.site.http.LoginResponse; import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.utils.AndroidUtils; -import javax.inject.Inject; - -import static org.floens.chan.Chan.getGraph; import static org.floens.chan.utils.AndroidUtils.getString; -public class PassSettingsController extends Controller implements View.OnClickListener, Site.LoginListener { - @Inject - ReplyManager replyManager; - - @Inject - HttpCallManager httpCallManager; - +public class LoginController extends Controller implements View.OnClickListener, Site.LoginListener { private LinearLayout container; private CrossfadeView crossfadeView; private TextView errors; @@ -61,17 +49,17 @@ public class PassSettingsController extends Controller implements View.OnClickLi private Site site; - public PassSettingsController(Context context) { + public LoginController(Context context) { super(context); } + public void setSite(Site site) { + this.site = site; + } + @Override public void onCreate() { super.onCreate(); - getGraph().inject(this); - - // TODO(multi-site) some selector of some sorts - site = Sites.defaultSite(); navigationItem.setTitle(R.string.settings_screen_pass); @@ -123,7 +111,6 @@ public class PassSettingsController extends Controller implements View.OnClickLi crossfadeView.toggle(true, true); button.setText(R.string.setting_pass_login); hideError(); - ((PassSettingControllerListener) previousSiblingController).onPassEnabledChanged(false); } else { auth(); } @@ -151,7 +138,6 @@ public class PassSettingsController extends Controller implements View.OnClickLi crossfadeView.toggle(false, true); button.setText(R.string.setting_pass_logout); authenticated.setText(response.message); - ((PassSettingControllerListener) previousSiblingController).onPassEnabledChanged(true); } private void authFail(LoginResponse response) { @@ -178,7 +164,6 @@ public class PassSettingsController extends Controller implements View.OnClickLi button.setText(R.string.setting_pass_logging_in); hideError(); - // TODO(multi-site) String user = inputToken.getText().toString(); String pass = inputPin.getText().toString(); site.login(new LoginRequest(user, pass), this); @@ -201,8 +186,4 @@ public class PassSettingsController extends Controller implements View.OnClickLi private boolean loggedIn() { return site.isLoggedIn(); } - - public interface PassSettingControllerListener { - void onPassEnabledChanged(boolean enabled); - } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java index 1588ac91..2b47d28e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java @@ -32,7 +32,6 @@ import org.floens.chan.R; import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.settings.ChanSettings; -import org.floens.chan.core.site.Sites; import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.RefreshUIMessage; @@ -60,14 +59,13 @@ import static org.floens.chan.Chan.getGraph; import static org.floens.chan.ui.theme.ThemeHelper.theme; import static org.floens.chan.utils.AndroidUtils.getString; -public class MainSettingsController extends SettingsController implements ToolbarMenuItem.ToolbarMenuItemCallback, WatchSettingsController.WatchSettingControllerListener, PassSettingsController.PassSettingControllerListener { +public class MainSettingsController extends SettingsController implements ToolbarMenuItem.ToolbarMenuItemCallback, WatchSettingsController.WatchSettingControllerListener { private static final int ADVANCED_SETTINGS = 1; private ListSettingView imageAutoLoadView; private ListSettingView videoAutoLoadView; private LinkSettingView saveLocation; private LinkSettingView watchLink; - private LinkSettingView passLink; private int clickCount; private SettingView developerView; private SettingView fontView; @@ -112,8 +110,6 @@ public class MainSettingsController extends SettingsController implements Toolba populatePreferences(); onWatchEnabledChanged(ChanSettings.watchEnabled.get()); - // TODO(multi-site) - onPassEnabledChanged(Sites.defaultSite().isLoggedIn()); buildPreferences(); @@ -188,11 +184,6 @@ public class MainSettingsController extends SettingsController implements Toolba watchLink.setDescription(enabled ? R.string.setting_watch_summary_enabled : R.string.setting_watch_summary_disabled); } - @Override - public void onPassEnabledChanged(boolean enabled) { - passLink.setDescription(enabled ? R.string.setting_pass_summary_enabled : R.string.setting_pass_summary_disabled); - } - private void populatePreferences() { // General group SettingsGroup general = new SettingsGroup(R.string.settings_group_general); @@ -319,13 +310,6 @@ public class MainSettingsController extends SettingsController implements Toolba // Posting group SettingsGroup posting = new SettingsGroup(R.string.settings_group_posting); - passLink = (LinkSettingView) posting.add(new LinkSettingView(this, R.string.settings_pass, 0, new View.OnClickListener() { - @Override - public void onClick(View v) { - navigationController.pushController(new PassSettingsController(context)); - } - })); - posting.add(new BooleanSettingView(this, ChanSettings.postPinThread, R.string.setting_post_pin, 0)); posting.add(new StringSettingView(this, ChanSettings.postDefaultName, R.string.setting_post_default_name, R.string.setting_post_default_name)); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java index 598e7c12..b6b39b9c 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/SiteSetupController.java @@ -18,7 +18,6 @@ package org.floens.chan.ui.controller; import android.content.Context; -import android.view.View; import org.floens.chan.R; import org.floens.chan.core.presenter.SiteSetupPresenter; @@ -37,6 +36,7 @@ public class SiteSetupController extends SettingsController implements SiteSetup private Site site; private LinkSettingView boardsLink; + private LinkSettingView loginLink; public SiteSetupController(Context context) { super(context); @@ -84,6 +84,34 @@ public class SiteSetupController extends SettingsController implements SiteSetup boardsLink.setDescription(descriptionText); } + @Override + public void setIsLoggedIn(boolean isLoggedIn) { + String text = context.getString(isLoggedIn ? + R.string.setup_site_login_description_enabled : + R.string.setup_site_login_description_enabled); + loginLink.setDescription(text); + } + + @Override + public void showLogin() { + SettingsGroup login = new SettingsGroup(R.string.setup_site_group_login); + + loginLink = new LinkSettingView( + this, + context.getString(R.string.setup_site_login), + "", + v -> { + LoginController loginController = new LoginController(context); + loginController.setSite(site); + navigationController.pushController(loginController); + } + ); + + login.add(loginLink); + + groups.add(login); + } + private void populatePreferences() { SettingsGroup general = new SettingsGroup(R.string.setup_site_group_general); @@ -91,13 +119,10 @@ public class SiteSetupController extends SettingsController implements SiteSetup this, context.getString(R.string.setup_site_boards), "", - new View.OnClickListener() { - @Override - public void onClick(View v) { - BoardSetupController boardSetupController = new BoardSetupController(context); - boardSetupController.setSite(site); - navigationController.pushController(boardSetupController); - } + v -> { + BoardSetupController boardSetupController = new BoardSetupController(context); + boardSetupController.setSite(site); + navigationController.pushController(boardSetupController); }); general.add(boardsLink); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/SitesSetupController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/SitesSetupController.java index c3aa8b57..637af738 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/SitesSetupController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/SitesSetupController.java @@ -21,7 +21,6 @@ package org.floens.chan.ui.controller; import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; -import android.graphics.drawable.Drawable; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; @@ -228,13 +227,17 @@ public class SitesSetupController extends StyledToolbarNavigationController impl public SiteCell(View itemView) { super(itemView); + + // Bind views image = itemView.findViewById(R.id.image); text = itemView.findViewById(R.id.text); description = itemView.findViewById(R.id.description); settings = itemView.findViewById(R.id.settings); + + // Setup views + itemView.setOnClickListener(this); setRoundItemBackground(settings); theme().settingsDrawable.apply(settings); - settings.setOnClickListener(this); } private void setSite(Site site) { @@ -243,19 +246,16 @@ public class SitesSetupController extends StyledToolbarNavigationController impl private void setSiteIcon(Site site) { siteIcon = site.icon(); - siteIcon.get(new SiteIcon.SiteIconResult() { - @Override - public void onSiteIcon(SiteIcon siteIcon, Drawable icon) { - if (SiteCell.this.siteIcon == siteIcon) { - image.setImageDrawable(icon); - } + siteIcon.get((siteIcon, icon) -> { + if (SiteCell.this.siteIcon == siteIcon) { + image.setImageDrawable(icon); } }); } @Override public void onClick(View v) { - if (v == settings) { + if (v == itemView) { onSiteCellSettingsClicked(site); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java index ac35d094..aecbc8f4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -253,29 +253,6 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply return true; } - @Override - public void setPage(ReplyPresenter.Page page, boolean animate) { - switch (page) { - case LOADING: - setWrap(true); - View progressBar = setView(null); - progressBar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dp(100))); - break; - case INPUT: - setView(replyInputLayout); - setWrap(!presenter.isExpanded()); - break; - case AUTHENTICATION: - setWrap(false); - - setView(captchaContainer); - - captchaContainer.requestFocus(View.FOCUS_DOWN); - - break; - } - } - @Override public void initializeAuthentication(Site site, Authentication authentication, AuthenticationLayoutCallback callback) { @@ -292,7 +269,16 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply break; } case GENERIC_WEBVIEW: { - authenticationLayout = new GenericWebViewAuthenticationLayout(getContext()); + GenericWebViewAuthenticationLayout view = new GenericWebViewAuthenticationLayout(getContext()); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ); +// params.setMargins(dp(8), dp(8), dp(8), dp(200)); + view.setLayoutParams(params); + + authenticationLayout = view; break; } case NONE: @@ -312,6 +298,34 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply authenticationLayout.reset(); } + @Override + public void setPage(ReplyPresenter.Page page, boolean animate) { + switch (page) { + case LOADING: + setWrap(true); + View progressBar = setView(null); + progressBar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dp(100))); + break; + case INPUT: + setView(replyInputLayout); + setWrap(!presenter.isExpanded()); + break; + case AUTHENTICATION: + setWrap(false); + + setView(captchaContainer); + + captchaContainer.requestFocus(View.FOCUS_DOWN); + + break; + } + + if (page != ReplyPresenter.Page.AUTHENTICATION && authenticationLayout != null) { + AndroidUtils.removeFromParentView((View) authenticationLayout); + authenticationLayout = null; + } + } + @Override public void resetAuthentication() { authenticationLayout.reset(); diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 099c7d96..41af8b97 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -179,6 +179,11 @@ Re-enable this permission in the app settings if you permanently disabled it."Setup boards %s added + Authentication + Login + Logged in + Off + Configure boards of %s Add board Removed \'%s\' @@ -501,8 +506,6 @@ Re-enable this permission in the app settings if you permanently disabled it."Click here to learn more. ]]> " - Using 4chan pass - Off Themes