xiongzhu 2 년 전
부모
커밋
69522c6996

+ 31 - 22
app/src/main/AndroidManifest.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
+    xmlns:tools="http://schemas.android.com/tools" >
 
     <uses-sdk tools:overrideLibrary="me.zhanghai.android.fastscroll" />
 
@@ -36,9 +36,18 @@
         android:roundIcon="@mipmap/ic_launcher"
         android:supportsRtl="true"
         android:theme="@style/DarkTheme"
-        android:usesCleartextTraffic="true">
+        android:usesCleartextTraffic="true" >
         <activity
-            android:name=".ReviewActivity"
+            android:name=".review.FakeZoomActivity"
+            android:configChanges="orientation|screenSize"
+            android:label="@string/title_activity_zoom"
+            android:parentActivityName=".GalleryActivity"
+            android:theme="@style/DarkTheme.NoTitle" />
+        <activity
+            android:name=".review.FakeGalleryActivity"
+            android:exported="false" />
+        <activity
+            android:name=".review.ReviewActivity"
             android:exported="false" />
         <activity
             android:name=".RegisterActivity"
@@ -52,20 +61,20 @@
             android:configChanges="orientation|screenSize"
             android:exported="true"
             android:label="@string/app_name"
-            android:theme="@style/DarkTheme">
+            android:theme="@style/DarkTheme" >
             <intent-filter>
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
 
         <activity-alias
-            android:name=".components.launcher.LauncherReal"
-            android:enabled="true"
+            android:name=".components.launcher.LauncherCalculator"
+            android:enabled="false"
             android:exported="true"
-            android:icon="@mipmap/ic_launcher"
-            android:label="@string/app_name"
-            android:roundIcon="@mipmap/ic_launcher"
-            android:targetActivity=".PINActivity">
+            android:icon="@mipmap/ic_launcher_calculator"
+            android:label="@string/app_name_fake_calculator"
+            android:roundIcon="@mipmap/ic_launcher_calculator_round"
+            android:targetActivity=".PINActivity" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
@@ -73,13 +82,13 @@
             </intent-filter>
         </activity-alias>
         <activity-alias
-            android:name=".components.launcher.LauncherCalculator"
-            android:enabled="false"
+            android:name=".components.launcher.LauncherReal"
+            android:enabled="true"
             android:exported="true"
-            android:icon="@mipmap/ic_launcher_calculator"
-            android:label="@string/app_name_fake_calculator"
-            android:roundIcon="@mipmap/ic_launcher_calculator_round"
-            android:targetActivity=".PINActivity">
+            android:icon="@mipmap/ic_launcher"
+            android:label="@string/app_name"
+            android:roundIcon="@mipmap/ic_launcher"
+            android:targetActivity=".PINActivity" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
@@ -99,7 +108,7 @@
             android:hardwareAccelerated="true"
             android:label="@string/app_name"
             android:parentActivityName=".PINActivity"
-            android:theme="@style/DarkTheme">
+            android:theme="@style/DarkTheme" >
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 
@@ -126,7 +135,7 @@
             android:exported="true"
             android:label="@string/title_activity_gallery"
             android:parentActivityName=".MainActivity"
-            android:theme="@style/DarkTheme">
+            android:theme="@style/DarkTheme" >
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 
@@ -161,7 +170,7 @@
             android:exported="true"
             android:label="@string/title_activity_tag_filter"
             android:parentActivityName=".MainActivity"
-            android:theme="@style/DarkTheme">
+            android:theme="@style/DarkTheme" >
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 
@@ -185,7 +194,7 @@
         <activity android:name=".SettingsActivity" />
         <activity
             android:name=".RandomActivity"
-            android:exported="true">
+            android:exported="true" >
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
 
@@ -209,7 +218,7 @@
         <activity
             android:name=".LoginActivity"
             android:label="@string/title_activity_login"
-            android:parentActivityName=".MainActivity">
+            android:parentActivityName=".MainActivity" >
             <meta-data
                 android:name="android.support.PARENT_ACTIVITY"
                 android:value="com.dar.nbook.MainActivity" />
@@ -219,7 +228,7 @@
             android:name="androidx.core.content.FileProvider"
             android:authorities="${applicationId}.provider"
             android:exported="false"
-            android:grantUriPermissions="true">
+            android:grantUriPermissions="true" >
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"
                 android:resource="@xml/provider_paths" />

+ 1 - 6
app/src/main/java/com/dar/nbook/PINActivity.java

@@ -3,7 +3,6 @@ package com.dar.nbook;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.graphics.Color;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -16,13 +15,8 @@ import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-import androidx.annotation.Nullable;
 import androidx.core.view.WindowCompat;
 
-import com.bumptech.glide.load.DataSource;
-import com.bumptech.glide.load.engine.GlideException;
-import com.bumptech.glide.request.RequestListener;
-import com.bumptech.glide.request.target.Target;
 import com.dar.nbook.api.HttpCallback;
 import com.dar.nbook.api.HttpClient;
 import com.dar.nbook.api.HttpError;
@@ -30,6 +24,7 @@ import com.dar.nbook.api.response.AppConfig;
 import com.dar.nbook.api.response.SysConfigResponse;
 import com.dar.nbook.components.GlideX;
 import com.dar.nbook.components.activities.GeneralActivity;
+import com.dar.nbook.review.ReviewActivity;
 import com.dar.nbook.settings.Global;
 
 import java.util.ArrayList;

+ 34 - 13
app/src/main/java/com/dar/nbook/api/response/FakeGallery.java

@@ -1,8 +1,9 @@
 package com.dar.nbook.api.response;
 
+import java.io.Serializable;
 import java.util.List;
 
-public class FakeGallery{
+public class FakeGallery implements Serializable {
     private String createdAt;
     private String thumb;
     private String artist;
@@ -10,51 +11,71 @@ public class FakeGallery{
     private int id;
     private String title;
 
-    public void setCreatedAt(String createdAt){
+    private String character;
+
+    private String tag;
+
+    public void setCreatedAt(String createdAt) {
         this.createdAt = createdAt;
     }
 
-    public String getCreatedAt(){
+    public String getCreatedAt() {
         return createdAt;
     }
 
-    public void setThumb(String thumb){
+    public void setThumb(String thumb) {
         this.thumb = thumb;
     }
 
-    public String getThumb(){
+    public String getThumb() {
         return thumb;
     }
 
-    public void setArtist(String artist){
+    public void setArtist(String artist) {
         this.artist = artist;
     }
 
-    public String getArtist(){
+    public String getArtist() {
         return artist;
     }
 
-    public void setDetails(List<String> details){
+    public void setDetails(List<String> details) {
         this.details = details;
     }
 
-    public List<String> getDetails(){
+    public List<String> getDetails() {
         return details;
     }
 
-    public void setId(int id){
+    public void setId(int id) {
         this.id = id;
     }
 
-    public int getId(){
+    public int getId() {
         return id;
     }
 
-    public void setTitle(String title){
+    public void setTitle(String title) {
         this.title = title;
     }
 
-    public String getTitle(){
+    public String getTitle() {
         return title;
     }
+
+    public String getCharacter() {
+        return character;
+    }
+
+    public void setCharacter(String character) {
+        this.character = character;
+    }
+
+    public String getTag() {
+        return tag;
+    }
+
+    public void setTag(String tag) {
+        this.tag = tag;
+    }
 }

+ 53 - 0
app/src/main/java/com/dar/nbook/review/FakeGalleryActivity.kt

@@ -0,0 +1,53 @@
+package com.dar.nbook.review
+
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
+import com.dar.nbook.R
+import com.dar.nbook.adapters.GalleryAdapter
+import com.dar.nbook.api.response.FakeGallery
+import com.dar.nbook.components.widgets.CustomGridLayoutManager
+import com.dar.nbook.databinding.ActivityFakeGalleryBinding
+import com.dar.nbook.review.adapters.FakeGalleryAdapter
+
+class FakeGalleryActivity : AppCompatActivity() {
+
+    private lateinit var binding: ActivityFakeGalleryBinding;
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = ActivityFakeGalleryBinding.inflate(layoutInflater)
+        val view = binding.root
+        setContentView(view)
+
+        val fakeGallery = intent.getSerializableExtra("fakeGallery") as FakeGallery
+        val adapter = FakeGalleryAdapter(fakeGallery)
+
+        val toolbar = binding.toolbar
+        setSupportActionBar(toolbar)
+        supportActionBar!!.setTitle(fakeGallery.title)
+        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+        supportActionBar!!.setDisplayShowTitleEnabled(true)
+
+        binding.recycler.layoutManager = CustomGridLayoutManager(this, 2)
+        binding.recycler.adapter = adapter
+        (binding.recycler.layoutManager as CustomGridLayoutManager).spanSizeLookup =
+            object : SpanSizeLookup() {
+                override fun getSpanSize(position: Int): Int {
+                    return when (position > 2) {
+                        true -> 1
+                        false -> 2
+                    }
+                }
+            }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (item.itemId == android.R.id.home) {
+            finish()
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+}

+ 493 - 0
app/src/main/java/com/dar/nbook/review/FakeZoomActivity.java

@@ -0,0 +1,493 @@
+package com.dar.nbook.review;
+
+import android.Manifest;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.Toolbar;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.bumptech.glide.Priority;
+import com.dar.nbook.R;
+import com.dar.nbook.api.HttpCallback;
+import com.dar.nbook.api.HttpClient;
+import com.dar.nbook.api.HttpError;
+import com.dar.nbook.api.components.GenericGallery;
+import com.dar.nbook.api.response.FakeGallery;
+import com.dar.nbook.api.response.HasInviteResponse;
+import com.dar.nbook.async.database.Queries;
+import com.dar.nbook.components.activities.GeneralActivity;
+import com.dar.nbook.files.GalleryFolder;
+import com.dar.nbook.settings.DefaultDialogs;
+import com.dar.nbook.settings.Global;
+import com.dar.nbook.settings.Login;
+import com.dar.nbook.utility.LogUtility;
+import com.dar.nbook.utility.Utility;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import java.io.File;
+
+public class FakeZoomActivity extends GeneralActivity {
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private final static int hideFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+        | View.SYSTEM_UI_FLAG_FULLSCREEN
+        | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY : 0);
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private final static int showFlags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+    private static final String VOLUME_SIDE_KEY = "volumeSide";
+    private static final String SCROLL_TYPE_KEY = "zoomScrollType";
+    private FakeGallery gallery;
+    private int actualPage = 0;
+    private boolean isHidden = false;
+    private ViewPager2 mViewPager;
+    private TextView pageManagerLabel, cornerPageViewer;
+    private View pageSwitcher;
+    private SeekBar seekBar;
+    private Toolbar toolbar;
+    private View view;
+    @ViewPager2.Orientation
+    private int tmpScrollType;
+    private boolean up = false, down = false, side;
+
+    private int pageCounter = 0;
+    private int pageThreshold = 10;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        //Global.initActivity(this);
+        SharedPreferences preferences = getSharedPreferences("Settings", 0);
+        side = preferences.getBoolean(VOLUME_SIDE_KEY, true);
+        setContentView(R.layout.activity_zoom);
+
+        //read arguments
+        gallery = (FakeGallery) getIntent().getSerializableExtra(getPackageName() + ".GALLERY");
+        final int page = getIntent().getExtras().getInt(getPackageName() + ".PAGE", 1) - 1;
+        //toolbar setup
+        toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        getSupportActionBar().setDisplayShowTitleEnabled(true);
+        setTitle(gallery.getTitle());
+
+        getWindow().setFlags(
+            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
+        if (Global.isLockScreen())
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+
+        //find views
+        SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(this);
+        mViewPager = findViewById(R.id.container);
+        mViewPager.setAdapter(mSectionsPagerAdapter);
+        mViewPager.setOrientation(preferences.getInt(SCROLL_TYPE_KEY, ScrollType.HORIZONTAL.ordinal()));
+        mViewPager.setOffscreenPageLimit(Global.getOffscreenLimit());
+        pageSwitcher = findViewById(R.id.page_switcher);
+        pageManagerLabel = findViewById(R.id.pages);
+        cornerPageViewer = findViewById(R.id.page_text);
+        seekBar = findViewById(R.id.seekBar);
+        view = findViewById(R.id.view);
+
+        //initial setup for views
+        changeLayout(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
+        mViewPager.setKeepScreenOn(Global.isLockScreen());
+        findViewById(R.id.prev).setOnClickListener(v -> changeClosePage(false));
+        findViewById(R.id.next).setOnClickListener(v -> changeClosePage(true));
+        seekBar.setMax(gallery.getDetails().size() - 1);
+        if (Global.useRtl()) {
+            seekBar.setRotationY(180);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                mViewPager.setLayoutDirection(ViewPager2.LAYOUT_DIRECTION_RTL);
+            }
+        }
+
+        //Adding listeners
+        mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+            @Override
+            public void onPageSelected(int newPage) {
+                int oldPage = actualPage;
+                actualPage = newPage;
+                LogUtility.d("Page selected: " + newPage + " from page " + oldPage);
+                setPageText(newPage + 1);
+                seekBar.setProgress(newPage);
+                clearFarRequests(oldPage, newPage);
+                makeNearRequests(newPage);
+                pageCounter++;
+                if (pageCounter >= pageThreshold) {
+                    pageCounter = 0;
+                    HttpClient.request(HttpClient.getApiService().hasInvite(Login.getNUser().getId()), new HttpCallback<HasInviteResponse>() {
+                        @Override
+                        public void onSuccess(HasInviteResponse data) {
+                            if (!data.isHasInvite()) {
+                                showInviteDialog();
+                            }
+                        }
+
+                        @Override
+                        public void onFailure(HttpError<HasInviteResponse> error) {
+                            showInviteDialog();
+                        }
+                    });
+
+                }
+            }
+        });
+        pageManagerLabel.setOnClickListener(v -> DefaultDialogs.pageChangerDialog(
+            new DefaultDialogs.Builder(this)
+                .setActual(actualPage + 1)
+                .setMin(1)
+                .setMax(gallery.getDetails().size())
+                .setTitle(R.string.change_page)
+                .setDrawable(R.drawable.ic_find_in_page)
+                .setDialogs(new DefaultDialogs.CustomDialogResults() {
+                    @Override
+                    public void positive(int actual) {
+                        changePage(actual - 1);
+                    }
+                })
+        ));
+        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (fromUser) {
+                    setPageText(progress + 1);
+                }
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                changePage(seekBar.getProgress());
+            }
+        });
+
+
+        changePage(page);
+        setPageText(page + 1);
+        seekBar.setProgress(page);
+    }
+
+    private void showInviteDialog() {
+        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(FakeZoomActivity.this);
+        builder.setTitle(R.string.share_to_your_friends);
+        builder.setMessage(R.string.share_to_your_friends_message);
+        builder.setPositiveButton(R.string.ok, (dialog, which) -> {
+            Global.invite(FakeZoomActivity.this);
+        });
+        builder.setCancelable(false);
+        builder.show();
+    }
+
+    private void setUserInput(boolean enabled) {
+        mViewPager.setUserInputEnabled(enabled);
+    }
+
+    private void setPageText(int page) {
+        pageManagerLabel.setText(getString(R.string.page_format, page, gallery.getDetails().size()));
+        cornerPageViewer.setText(getString(R.string.page_format, page, gallery.getDetails().size()));
+    }
+
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (Global.volumeOverride()) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_VOLUME_UP:
+                    up = false;
+                    return true;
+                case KeyEvent.KEYCODE_VOLUME_DOWN:
+                    down = false;
+                    return true;
+            }
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (Global.volumeOverride()) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_VOLUME_UP:
+                    up = true;
+                    changeClosePage(side);
+                    if (up && down) changeSide();
+                    return true;
+                case KeyEvent.KEYCODE_VOLUME_DOWN:
+                    down = true;
+                    changeClosePage(!side);
+                    if (up && down) changeSide();
+                    return true;
+            }
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    private void changeSide() {
+        getSharedPreferences("Settings", 0).edit().putBoolean(VOLUME_SIDE_KEY, side = !side).apply();
+        Toast.makeText(this, side ? R.string.next_page_volume_up : R.string.next_page_volume_down, Toast.LENGTH_SHORT).show();
+    }
+
+    public void changeClosePage(boolean next) {
+        if (Global.useRtl()) next = !next;
+        if (next && mViewPager.getCurrentItem() < (mViewPager.getAdapter().getItemCount() - 1))
+            changePage(mViewPager.getCurrentItem() + 1);
+        if (!next && mViewPager.getCurrentItem() > 0) changePage(mViewPager.getCurrentItem() - 1);
+    }
+
+    @Override
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        changeLayout(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE);
+    }
+
+    private boolean hardwareKeys() {
+        return ViewConfiguration.get(this).hasPermanentMenuKey();
+    }
+
+    private void applyMargin(boolean landscape, View view) {
+        ConstraintLayout.LayoutParams lp = (ConstraintLayout.LayoutParams) view.getLayoutParams();
+        lp.setMargins(0, 0, landscape && !hardwareKeys() ? Global.getNavigationBarHeight(this) : 0, 0);
+        view.setLayoutParams(lp);
+    }
+
+    public ViewPager2 geViewPager() {
+        return mViewPager;
+    }
+
+    private void changeLayout(boolean landscape) {
+        int statusBarHeight = Global.getStatusBarHeight(this);
+        applyMargin(landscape, findViewById(R.id.master_layout));
+        applyMargin(landscape, toolbar);
+        pageSwitcher.setPadding(0, 0, 0, landscape ? 0 : statusBarHeight);
+    }
+
+    private void changePage(int newPage) {
+        mViewPager.setCurrentItem(newPage);
+    }
+
+    private void changeScrollTypeDialog() {
+        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+        int scrollType = mViewPager.getOrientation();
+        tmpScrollType = mViewPager.getOrientation();
+        builder.setTitle(getString(R.string.change_scroll_type) + ":");
+        builder.setSingleChoiceItems(R.array.scroll_type, scrollType, (dialog, which) -> tmpScrollType = which);
+        builder.setPositiveButton(R.string.ok, (dialog, which) -> {
+            if (tmpScrollType != scrollType) {
+                mViewPager.setOrientation(tmpScrollType);
+                getSharedPreferences("Settings", 0).edit().putInt(SCROLL_TYPE_KEY, tmpScrollType).apply();
+                int page = actualPage;
+                changePage(page + 1);
+                changePage(page);
+            }
+        }).setNegativeButton(R.string.cancel, null);
+        builder.show();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_zoom, menu);
+        Utility.tintMenu(menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+        if (id == R.id.rotate) {
+            getActualFragment().rotate();
+        } else if (id == R.id.save_page) {
+            if (Global.hasStoragePermission(this)) {
+                downloadPage();
+            } else requestStorage();
+        } else if (id == R.id.share) {
+            if (gallery.getId() <= 0) sendImage(false);
+            else openSendImageDialog();
+        } else if (id == android.R.id.home) {
+            finish();
+            return true;
+        } else if (id == R.id.bookmark) {
+            Queries.ResumeTable.insert(gallery.getId(), actualPage + 1);
+        } else if (id == R.id.scrollType) {
+            changeScrollTypeDialog();
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void openSendImageDialog() {
+        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+        builder.setPositiveButton(R.string.yes, (dialog, which) -> sendImage(true))
+            .setNegativeButton(R.string.no, (dialog, which) -> sendImage(false))
+            .setCancelable(true).setTitle(R.string.send_with_title)
+            .setMessage(R.string.caption_send_with_title)
+            .show();
+    }
+
+    @TargetApi(Build.VERSION_CODES.M)
+    private void requestStorage() {
+        requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        Global.initStorage(this);
+        if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
+            downloadPage();
+
+    }
+
+    private FakeZoomFragment getActualFragment() {
+        return getActualFragment(mViewPager.getCurrentItem());
+    }
+
+    private void makeNearRequests(int newPage) {
+        FakeZoomFragment fragment;
+        int offScreenLimit = Global.getOffscreenLimit();
+        for (int i = newPage - offScreenLimit; i <= newPage + offScreenLimit; i++) {
+            fragment = getActualFragment(i);
+            if (fragment == null) continue;
+            if (i == newPage) fragment.loadImage(Priority.IMMEDIATE);
+            else fragment.loadImage();
+        }
+    }
+
+    private void clearFarRequests(int oldPage, int newPage) {
+        FakeZoomFragment fragment;
+        int offScreenLimit = Global.getOffscreenLimit();
+        for (int i = oldPage - offScreenLimit; i <= oldPage + offScreenLimit; i++) {
+            if (i >= newPage - offScreenLimit && i <= newPage + offScreenLimit) continue;
+            fragment = getActualFragment(i);
+            if (fragment == null) continue;
+            fragment.cancelRequest();
+        }
+
+    }
+
+    private FakeZoomFragment getActualFragment(int position) {
+        return (FakeZoomFragment) getSupportFragmentManager().findFragmentByTag("f" + position);
+    }
+
+    private void sendImage(boolean withText) {
+        int pageNum = mViewPager.getCurrentItem();
+        Utility.sendImage(this, getActualFragment().getDrawable(), withText ? gallery.getDetails().get(pageNum) : null);
+    }
+
+    private void downloadPage() {
+        final File output = new File(Global.SCREENFOLDER, gallery.getId() + "-" + (mViewPager.getCurrentItem() + 1) + ".jpg");
+        Utility.saveImage(getActualFragment().getDrawable(), output);
+    }
+
+    private void animateLayout() {
+
+        AnimatorListenerAdapter adapter = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (isHidden) {
+                    pageSwitcher.setVisibility(View.GONE);
+                    toolbar.setVisibility(View.GONE);
+                    view.setVisibility(View.GONE);
+                    cornerPageViewer.setVisibility(View.VISIBLE);
+                }
+            }
+        };
+
+        pageSwitcher.setVisibility(View.VISIBLE);
+        toolbar.setVisibility(View.VISIBLE);
+        view.setVisibility(View.VISIBLE);
+        cornerPageViewer.setVisibility(View.GONE);
+
+        pageSwitcher.animate().alpha(isHidden ? 0f : 0.75f).setDuration(150).setListener(adapter).start();
+        view.animate().alpha(isHidden ? 0f : 0.75f).setDuration(150).setListener(adapter).start();
+        toolbar.animate().alpha(isHidden ? 0f : 0.75f).setDuration(150).setListener(adapter).start();
+    }
+
+    private void applyVisibilityFlag() {
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+            getWindow().getDecorView().setSystemUiVisibility(isHidden ? hideFlags : showFlags);
+        } else {
+            getWindow().addFlags(isHidden ? WindowManager.LayoutParams.FLAG_FULLSCREEN : WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
+            getWindow().clearFlags(isHidden ? WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN : WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        }
+    }
+
+    private enum ScrollType {HORIZONTAL, VERTICAL}
+
+    public class SectionsPagerAdapter extends FragmentStateAdapter {
+        public SectionsPagerAdapter(FakeZoomActivity activity) {
+            super(activity.getSupportFragmentManager(), activity.getLifecycle());
+
+        }
+
+        private boolean allowScroll = true;
+
+        @NonNull
+        @Override
+        public Fragment createFragment(int position) {
+            FakeZoomFragment f = FakeZoomFragment.newInstance(gallery, position);
+
+            f.setZoomChangeListener((v, zoomLevel) -> {
+                try {
+                    boolean _allowScroll = zoomLevel < 1.1f;
+                    if (_allowScroll != allowScroll) {
+                        setUserInput(!allowScroll);
+                        allowScroll = _allowScroll;
+                    }
+                } catch (Exception ex) {
+                }
+            });
+
+            f.setClickListener(v -> {
+                isHidden = !isHidden;
+                LogUtility.d("Clicked " + isHidden);
+                applyVisibilityFlag();
+                animateLayout();
+            });
+            return f;
+        }
+
+        @Override
+        public int getItemCount() {
+            return gallery.getDetails().size();
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+    }
+}

+ 248 - 0
app/src/main/java/com/dar/nbook/review/FakeZoomFragment.java

@@ -0,0 +1,248 @@
+package com.dar.nbook.review;
+
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.RequestManager;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.load.resource.bitmap.Rotate;
+import com.bumptech.glide.load.resource.gif.GifDrawable;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.ImageViewTarget;
+import com.bumptech.glide.request.target.Target;
+import com.bumptech.glide.request.transition.Transition;
+import com.dar.nbook.R;
+import com.dar.nbook.api.response.FakeGallery;
+import com.dar.nbook.components.GlideX;
+import com.dar.nbook.files.PageFile;
+import com.dar.nbook.github.chrisbanes.photoview.PhotoView;
+import com.dar.nbook.settings.Global;
+import com.dar.nbook.utility.LogUtility;
+
+public class FakeZoomFragment extends Fragment {
+
+    public interface OnZoomChangeListener {
+        void onZoomChange(View v, float zoomLevel);
+    }
+
+    private static final float MAX_SCALE = 4f;
+    private static final float CHANGE_PAGE_THRESHOLD = .2f;
+    private PhotoView photoView = null;
+    private ImageButton retryButton;
+    private PageFile pageFile = null;
+    private Uri url;
+    private int degree = 0;
+    private boolean completedDownload = false;
+    private View.OnClickListener clickListener;
+    private OnZoomChangeListener zoomChangeListener;
+    private ImageViewTarget<Drawable> target = null;
+
+
+    public FakeZoomFragment() {
+    }
+
+    public static FakeZoomFragment newInstance(FakeGallery gallery, int page) {
+        Bundle args = new Bundle();
+        args.putString("URL", gallery.getDetails().get(page));
+        FakeZoomFragment fragment = new FakeZoomFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    private boolean isValueApproximate(final float expectedValue, final float value) {
+        final float errorMargin = 0.05f;
+        return Math.abs(expectedValue - value) < errorMargin;
+    }
+
+    public void setClickListener(View.OnClickListener clickListener) {
+        this.clickListener = clickListener;
+    }
+
+
+    public void setZoomChangeListener(OnZoomChangeListener zoomChangeListener) {
+        this.zoomChangeListener = zoomChangeListener;
+    }
+
+    private float calculateScaleFactor(int width, int height) {
+        FragmentActivity activity = getActivity();
+        if (height < width * 2) return Global.getDefaultZoom();
+        float finalSize =
+            ((float) Global.getDeviceWidth(activity) * height) /
+                ((float) Global.getDeviceHeight(activity) * width);
+        finalSize = Math.max(finalSize, Global.getDefaultZoom());
+        finalSize = Math.min(finalSize, MAX_SCALE);
+        LogUtility.d("Final scale: " + finalSize);
+        return (float) Math.floor(finalSize);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_zoom, container, false);
+        FakeZoomActivity activity = (FakeZoomActivity) getActivity();
+        assert getArguments() != null;
+        assert activity != null;
+        //find views
+        photoView = rootView.findViewById(R.id.image);
+        retryButton = rootView.findViewById(R.id.imageView);
+        //read arguments
+        String str = getArguments().getString("URL");
+        url = str == null ? null : Uri.parse(str);
+        photoView.setAllowParentInterceptOnEdge(true);
+        photoView.setOnPhotoTapListener((view, x, y) -> {
+            boolean prev = x < CHANGE_PAGE_THRESHOLD;
+            boolean next = x > 1f - CHANGE_PAGE_THRESHOLD;
+            if ((prev || next) && Global.isButtonChangePage()) {
+                activity.changeClosePage(next);
+            } else if (clickListener != null) {
+                clickListener.onClick(view);
+            }
+            LogUtility.d(view, x, y, prev, next);
+        });
+
+        photoView.setOnScaleChangeListener((float scaleFactor, float focusX, float focusY) -> {
+            if (this.zoomChangeListener != null) {
+                this.zoomChangeListener.onZoomChange(rootView, photoView.getScale());
+            }
+        });
+
+        photoView.setMaximumScale(MAX_SCALE);
+        retryButton.setOnClickListener(v -> loadImage());
+        createTarget();
+        loadImage();
+        return rootView;
+    }
+
+    private void createTarget() {
+        target = new ImageViewTarget<Drawable>(photoView) {
+
+            @Override
+            protected void setResource(@Nullable Drawable resource) {
+                photoView.setImageDrawable(resource);
+            }
+
+            void applyDrawable(ImageView toShow, ImageView toHide, Drawable drawable) {
+                toShow.setVisibility(View.VISIBLE);
+                toHide.setVisibility(View.GONE);
+                toShow.setImageDrawable(drawable);
+                if (toShow instanceof PhotoView)
+                    scalePhoto(drawable);
+            }
+
+            @Override
+            public void onLoadStarted(@Nullable Drawable placeholder) {
+                super.onLoadStarted(placeholder);
+                applyDrawable(photoView, retryButton, placeholder);
+            }
+
+            @Override
+            public void onLoadFailed(@Nullable Drawable errorDrawable) {
+                super.onLoadFailed(errorDrawable);
+                applyDrawable(retryButton, photoView, errorDrawable);
+            }
+
+            @Override
+            public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
+                applyDrawable(photoView, retryButton, resource);
+                if (resource instanceof Animatable)
+                    ((GifDrawable) resource).start();
+            }
+
+            @Override
+            public void onLoadCleared(@Nullable Drawable placeholder) {
+                super.onLoadCleared(placeholder);
+                applyDrawable(photoView, retryButton, placeholder);
+            }
+        };
+    }
+
+    private void scalePhoto(Drawable drawable) {
+        photoView.setScale(calculateScaleFactor(
+            drawable.getIntrinsicWidth(),
+            drawable.getIntrinsicHeight()
+        ), 0, 0, false);
+    }
+
+    public void loadImage() {
+        loadImage(Priority.NORMAL);
+    }
+
+    public void loadImage(Priority priority) {
+        if (completedDownload) return;
+        cancelRequest();
+        RequestBuilder<Drawable> dra = loadPage();
+        if (dra == null) return;
+        dra
+            .transform(new Rotate(degree))
+            .placeholder(R.drawable.ic_launcher_foreground)
+            .error(R.drawable.ic_refresh)
+            .priority(priority)
+            .addListener(new RequestListener<Drawable>() {
+                @Override
+                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
+                    return false;
+                }
+
+                @Override
+                public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+                    completedDownload = true;
+                    return false;
+                }
+            })
+            .into(target);
+    }
+
+    @Nullable
+    private RequestBuilder<Drawable> loadPage() {
+        RequestBuilder<Drawable> request;
+        RequestManager glide = GlideX.with(photoView);
+        if (glide == null) return null;
+        if (pageFile != null) {
+            request = glide.load(pageFile);
+            LogUtility.d("Requested file glide: " + pageFile);
+        } else {
+            if (url == null) request = glide.load(R.mipmap.ic_launcher);
+            else {
+                LogUtility.d("Requested url glide: " + url);
+                request = glide.load(url);
+            }
+        }
+        return request;
+    }
+
+    public Drawable getDrawable() {
+        return photoView.getDrawable();
+    }
+
+    public void cancelRequest() {
+        if (completedDownload) return;
+        if (photoView != null && target != null) {
+            RequestManager manager = GlideX.with(photoView);
+            if (manager != null) manager.clear(target);
+        }
+    }
+
+    private void updateDegree() {
+        degree = (degree + 270) % 360;
+        loadImage();
+    }
+
+    public void rotate() {
+        updateDegree();
+    }
+}

+ 2 - 7
app/src/main/java/com/dar/nbook/ReviewActivity.kt → app/src/main/java/com/dar/nbook/review/ReviewActivity.kt

@@ -1,19 +1,14 @@
-package com.dar.nbook
+package com.dar.nbook.review
 
 import androidx.appcompat.app.AppCompatActivity
 import android.os.Bundle
-import androidx.recyclerview.widget.RecyclerView
-import com.dar.nbook.adapters.FakeAdapter
+import com.dar.nbook.review.adapters.FakeAdapter
 import com.dar.nbook.api.HttpCallback
 import com.dar.nbook.api.HttpClient
 import com.dar.nbook.api.HttpError
-import com.dar.nbook.api.components.NUser
 import com.dar.nbook.api.response.FakeGallery
 import com.dar.nbook.api.response.GalleryResponse
-import com.dar.nbook.databinding.ActivityBloginBinding
 import com.dar.nbook.databinding.ActivityReviewBinding
-import com.dar.nbook.settings.Login
-import com.dar.nbook.utility.snackError
 import splitties.views.recyclerview.gridLayoutManager
 
 class ReviewActivity : AppCompatActivity() {

+ 14 - 3
app/src/main/java/com/dar/nbook/adapters/FakeAdapter.kt → app/src/main/java/com/dar/nbook/review/adapters/FakeAdapter.kt

@@ -1,14 +1,17 @@
-package com.dar.nbook.adapters
+package com.dar.nbook.review.adapters
 
-import android.content.Context
+import android.content.Intent
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import androidx.recyclerview.widget.RecyclerView
 import com.bumptech.glide.Glide
 import com.dar.nbook.R
+import com.dar.nbook.RegisterActivity
 import com.dar.nbook.api.response.FakeGallery
-import com.dar.nbook.components.GlideX
 import com.dar.nbook.databinding.EntryLayoutBinding
+import com.dar.nbook.review.FakeGalleryActivity
+import splitties.activities.start
+import splitties.views.onClick
 
 class FakeAdapter(private val dataSet: List<FakeGallery>) :
     RecyclerView.Adapter<FakeAdapter.ViewHolder>() {
@@ -27,6 +30,14 @@ class FakeAdapter(private val dataSet: List<FakeGallery>) :
                 .placeholder(R.drawable.ic_logo)
                 .into(binding.image)
             binding.title.text = fakeGallery.title
+            binding.masterLayout.setOnClickListener {
+                val intent = Intent(
+                    binding.root.context,
+                    FakeGalleryActivity::class.java
+                )
+                intent.putExtra("fakeGallery", fakeGallery)
+                binding.root.context.startActivity(intent)
+            }
         }
     }
 

+ 131 - 0
app/src/main/java/com/dar/nbook/review/adapters/FakeGalleryAdapter.kt

@@ -0,0 +1,131 @@
+package com.dar.nbook.review.adapters
+
+import android.content.Intent
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.dar.nbook.R
+import com.dar.nbook.ZoomActivity
+import com.dar.nbook.api.response.FakeGallery
+import com.dar.nbook.databinding.ChipLayoutBinding
+import com.dar.nbook.databinding.ImageVoidBinding
+import com.dar.nbook.databinding.SubTagLayoutBinding
+import com.dar.nbook.review.FakeZoomActivity
+
+class FakeGalleryAdapter(private val dataSet: FakeGallery) :
+    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+
+    class TagViewHolder(binding: SubTagLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
+        private var binding: SubTagLayoutBinding
+
+        init {
+            this.binding = binding
+        }
+
+        fun bind(title: String, items: List<String>) {
+            binding.title.text = title
+            binding.chipGroup.removeAllViews()
+            items.forEach {
+                val c = ChipLayoutBinding.inflate(
+                    LayoutInflater.from(binding.root.context),
+                    binding.chipGroup,
+                    false
+                )
+                c.root.text = it
+                binding.chipGroup.addView(c.root)
+            }
+        }
+    }
+
+    class ImageViewHolder(binding: ImageVoidBinding) : RecyclerView.ViewHolder(binding.root) {
+        private var binding: ImageVoidBinding
+
+        init {
+            this.binding = binding
+        }
+
+        fun bind(position: Int, gallery: FakeGallery) {
+            Glide.with(binding.root.context)
+                .load(gallery.details[position])
+                .placeholder(R.drawable.ic_logo)
+                .into(binding.image)
+            binding.pageNumber.text = (position + 1).toString()
+            binding.master.setOnClickListener {
+
+                val intent = Intent(binding.root.context, FakeZoomActivity::class.java)
+                intent.putExtra(binding.root.context.packageName + ".GALLERY", gallery)
+                intent.putExtra(binding.root.context.packageName + ".PAGE", position + 1)
+                binding.root.context.startActivity(intent)
+            }
+        }
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        if (position <= 2) {
+            return 0
+        }
+        return 1
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+        when (viewType) {
+            0 -> {
+                val itemBinding =
+                    SubTagLayoutBinding.inflate(
+                        LayoutInflater.from(parent.context),
+                        parent,
+                        false
+                    )
+                return TagViewHolder(itemBinding)
+            }
+
+            else -> {
+                val itemBinding =
+                    ImageVoidBinding.inflate(
+                        LayoutInflater.from(parent.context),
+                        parent,
+                        false
+                    )
+                return ImageViewHolder(itemBinding)
+            }
+        }
+    }
+
+    override fun getItemCount(): Int {
+        return dataSet.details.size + 3
+    }
+
+    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+        when (holder) {
+            is TagViewHolder -> {
+                when (position) {
+                    0 -> {
+                        holder.bind(
+                            holder.itemView.context.getString(R.string.artists),
+                            dataSet.artist.split(" ")
+                        )
+                    }
+
+                    1 -> {
+                        holder.bind(
+                            holder.itemView.context.getString(R.string.tags),
+                            dataSet.tag.split(" ")
+                        )
+                    }
+
+                    2 -> {
+                        holder.bind(
+                            holder.itemView.context.getString(R.string.characters),
+                            dataSet.character.split(" ")
+                        )
+                    }
+                }
+            }
+
+            is ImageViewHolder -> {
+                holder.bind(position - 3, dataSet)
+            }
+        }
+    }
+}

+ 41 - 0
app/src/main/res/layout/activity_fake_gallery.xml

@@ -0,0 +1,41 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".review.FakeGalleryActivity">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar"
+        android:layout_width="match_parent"
+        android:layout_height="192dp"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <net.opacapp.multilinecollapsingtoolbar.CollapsingToolbarLayout
+            android:id="@+id/collapsing"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:contentScrim="?attr/colorPrimaryVariant"
+            app:expandedTitleTextAppearance="@style/TextAppearance.AppCompat.Large"
+            app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
+            app:maxLines="4"
+            app:toolbarId="@+id/toolbar">
+
+
+            <androidx.appcompat.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:layout_collapseMode="pin" />
+        </net.opacapp.multilinecollapsingtoolbar.CollapsingToolbarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recycler"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:background="?attr/colorPrimaryVariant"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/appbar" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 1 - 1
app/src/main/res/layout/activity_review.xml

@@ -4,7 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".ReviewActivity">
+    tools:context=".review.ReviewActivity">
 
     <com.google.android.material.appbar.AppBarLayout
         android:id="@+id/appBar"

+ 21 - 0
app/src/main/res/layout/fake_tag_layout.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/master"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/tag_master"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <include layout="@layout/sub_tag_layout" />
+
+        <include layout="@layout/sub_tag_layout" />
+
+        <include layout="@layout/sub_tag_layout" />
+
+    </LinearLayout>
+</LinearLayout>