M10: store assets - Plain Files naming, generated icon/feature graphic, listing copy
This commit is contained in:
@@ -31,6 +31,21 @@ upload needs a **higher `versionCode`** than every previous upload.
|
||||
|
||||
### Added
|
||||
|
||||
- **M10 — store assets.** The app is named **Plain Files** (one string; the `applicationId`
|
||||
was already locked in M0). `tools/make-store-assets.sh` renders the 512×512 RGBA icon and
|
||||
the 1024×500 RGB feature graphic from the app's own icon vectors, so the store icon *is*
|
||||
the launcher icon rather than a lookalike that drifts after the next rebrand.
|
||||
`tools/make-store-screenshots.sh` normalises `adb` captures to Play's 2:1 limit per form
|
||||
factor — a phone at its native 1080×2400 is 2.22:1 and would be rejected outright.
|
||||
[docs/STORE_LISTING.md](docs/STORE_LISTING.md) has the ready-to-paste copy, length-checked
|
||||
against Play's limits, plus the permissions paragraph that the `MANAGE_EXTERNAL_STORAGE`
|
||||
declaration requires. The feature graphic's copy was verified to survive Play's
|
||||
centre-square crop by cropping the rendered PNG, not by trusting the arithmetic.
|
||||
|
||||
**Not finished:** the screenshots themselves. The generator and the raw-capture
|
||||
directories are in place, but no captures have been taken, so `store-assets/screenshots/`
|
||||
is still empty. The listing is publishable except for that.
|
||||
|
||||
- **M9 — monetisation.** Decided: a **one-time paid app at €4.49, with a 14-day launch price
|
||||
of €2.29**, both set in the Play Console. No ads, no subscription and no in-app purchases —
|
||||
which means no Billing client, no `com.android.vending.BILLING` permission and no purchase
|
||||
|
||||
@@ -12,10 +12,10 @@ no advertising or tracking SDKs.
|
||||
|
||||
## Status
|
||||
|
||||
**M9 — monetisation: done.** One-time paid at €4.49 (€2.29 for the first 14 days), set in
|
||||
the Play Console — so the APK carries no Billing client and no `BILLING` permission, which
|
||||
keeps the offline claim true of the shipped artefact. See
|
||||
[docs/MONETISATION.md](docs/MONETISATION.md).
|
||||
**M10 — store assets: mostly done.** The app is named **Plain Files**, the icon and feature
|
||||
graphic are generated from the app's own vectors, the screenshot normaliser is written, and
|
||||
[docs/STORE_LISTING.md](docs/STORE_LISTING.md) holds length-checked listing copy. The
|
||||
screenshots are still outstanding — the generator is ready but no captures exist yet.
|
||||
|
||||
## Tech stack
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--
|
||||
All user-visible text lives here. English is currently the only locale, but keeping the
|
||||
text in resources is what makes a translation possible later at all — Play's automatic
|
||||
translation covers the store listing, not the app UI. Plurals are declared properly so a
|
||||
language with more than two plural forms needs no code change.
|
||||
|
||||
NOTE: `app_name` is a working title. The Play listing needs a distinctive name before
|
||||
the first upload (it is changeable afterwards, unlike the applicationId).
|
||||
The Play listing name. Deliberately not "File Manager": dozens of apps are literally
|
||||
called that, and a distinctive name is what makes the listing findable and
|
||||
rememberable. "Plain" carries the pitch — no ads, no account, no clutter.
|
||||
-->
|
||||
<string name="app_name">File Manager</string>
|
||||
<string name="app_name">Plain Files</string>
|
||||
|
||||
<!-- Progress notification for long file operations -->
|
||||
<string name="operation_copy_title">Copying files</string>
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
|
||||
| M7 | On-device validation & hardening | ✅ | Run on a real **Xiaomi M2101K7AG** (Android 12 / MIUI V140) — see [EDGE_CASE_TESTING](EDGE_CASE_TESTING.md). Verified on hardware: the first-run permission prompt, granting "All files access" while the app is alive (picked up on resume), browsing real storage, the text editor's read **and** write path (independently checked with `adb shell cat`), the storage-usage screen, landscape layout, and **graceful degradation when access is revoked mid-use**. Found and fixed two real defects: misleading first-run copy ("Full access was declined" on a device where nothing had been declined), and a usage screen that stayed blank for **~45–60 s** on a 55 GB volume — now one listing per directory instead of a `stat` per node, with rows streamed out as they are measured. **142 JVM tests** |
|
||||
| M8 | Localisation groundwork | ✅ | Every user-visible string is in `res/values/strings.xml` with real `<plurals>`, and `text/TextRes.kt` maps **both** `OperationKind` and `FileKind` onto resources so the screen, the notification and the details sheet cannot drift. The audit found a genuine miss: the details sheet rendered `FileKind.name`, i.e. untranslatable English — now a resource. `isPseudoLocalesEnabled = true` on the debug build makes "did every string come from resources?" mechanically checkable; verified `en-rXA` is present in the debug APK and the flag is debug-only. Byte units and the timestamp pattern are deliberately unlocalised, and the README says why. **142 JVM tests** |
|
||||
| M9 | Monetisation | ✅ | **Decided: one-time paid — €4.49 base with a 14-day launch price of €2.29**, both set in the Play Console (*Products › App pricing*), so there is **no Billing client, no `com.android.vending.BILLING` permission and no purchase code**. Rationale, market anchors and Console steps: [MONETISATION](MONETISATION.md). Verified against the release APK: zero billing and zero `INTERNET` references. **The price must be set before the first upload** — Play allows paid → free but never free → paid |
|
||||
| M10 | Store assets | ⏳ | icon/feature-graphic generators, per-form-factor screenshots, [STORE_LISTING](STORE_LISTING.md); display name confirmed |
|
||||
| M10 | Store assets | 🚧 | Name decided: **Plain Files** (one string; `applicationId` already locked). `tools/make-store-assets.sh` renders the 512×512 **RGBA** icon and the 1024×500 **RGB** feature graphic from the app's own vectors, so the store icon *is* the launcher icon; `tools/make-store-screenshots.sh` normalises `adb` captures to Play's 2:1 cap per form factor (phone crop default 93/130 px, tablets 0). [STORE_LISTING](STORE_LISTING.md) carries length-checked copy (name 11/30, short 67/80, full 1881/4000), the permissions paragraph the `MANAGE_EXTERNAL_STORAGE` declaration requires, and the graphics table. The feature graphic's copy was verified to survive Play's centre-square crop by cropping the rendered PNG. **Outstanding: screenshots** — the tooling and directories are in place but no captures exist yet |
|
||||
|
||||
## Key decisions
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Play Store listing (M10)
|
||||
|
||||
Ready-to-paste copy for the *Main store listing* page, plus the graphics that go with it.
|
||||
Every graphic is generated from the app's own vectors, so a rebrand is a re-run rather than
|
||||
a redraw.
|
||||
|
||||
See [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) § 6 for where this fits in the
|
||||
release.
|
||||
|
||||
## App name
|
||||
|
||||
**Plain Files**
|
||||
|
||||
Deliberately not "File Manager": dozens of apps are literally called that, and a distinctive
|
||||
name is what makes a listing findable and rememberable. It is one string
|
||||
(`app_name` in `values/strings.xml`); the `applicationId` is separate and already locked.
|
||||
|
||||
## Graphics
|
||||
|
||||
```bash
|
||||
./tools/make-store-assets.sh # icon + feature graphic -> store-assets/
|
||||
./tools/make-store-screenshots.sh # phone -> screenshots/phone/
|
||||
FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh
|
||||
FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh
|
||||
```
|
||||
|
||||
| Asset | Play's spec | Result | Source |
|
||||
|---|---|---|---|
|
||||
| App icon | 512×512, 32-bit PNG with alpha, ≤ 1 MB | `store-assets/icon-512.png` — 512×512, **RGBA**, ~5 KB | `tools/store-assets/icon.svg`, mirroring `ic_launcher_background.xml` + `ic_launcher_foreground.xml`. Background, paths and opacities are identical, so **the store icon is the launcher icon** rather than a lookalike that drifts. |
|
||||
| Feature graphic | 1024×500, JPEG or 24-bit PNG **without** alpha | `store-assets/feature-graphic-1024x500.png` — 1024×500, **RGB** | `tools/store-assets/feature-graphic.svg` |
|
||||
| Phone screenshots | no alpha, shorter side ≥ 320, longer side ≤ 3840, **longer side ≤ 2× shorter** | `store-assets/screenshots/phone/` (1080×2160) | `tools/make-store-screenshots.sh`, from `adb` captures in `store-assets/raw-screenshots/phone/` |
|
||||
|
||||
Two details worth keeping:
|
||||
|
||||
- **The icon is full bleed.** The background rect covers all 108 dp, so every pixel is
|
||||
opaque and Play's mask can crop to any shape without showing a gap.
|
||||
- **A phone at its native 1080×2400 is 2.22:1 and is therefore rejected.** The normaliser
|
||||
crops the status and navigation bars and then clamps to 1080×2160 — system chrome only,
|
||||
never app content. Tablets are already inside the 2:1 cap, so their crop defaults to 0.
|
||||
|
||||
### The feature graphic's safe area
|
||||
|
||||
Play crops the feature graphic to a **centre square** for some placements, so all of the copy
|
||||
sits inside **x 262…762** and the motif — which does not survive — is decoration. Verified by
|
||||
actually cropping the rendered PNG rather than by trusting the arithmetic:
|
||||
|
||||
```bash
|
||||
magick store-assets/feature-graphic-1024x500.png -gravity Center -crop 500x500+0+0 +repage /tmp/square.png
|
||||
```
|
||||
|
||||
## Short description (80 characters max)
|
||||
|
||||
```
|
||||
Browse, organise and manage your files. Ad-free, offline, pay once.
|
||||
```
|
||||
|
||||
## Full description (4000 characters max)
|
||||
|
||||
```
|
||||
Plain Files is a straightforward file manager for Android — no ads, no account, no
|
||||
subscription, and no network access at all. You buy it once and it is yours.
|
||||
|
||||
BROWSE ANYTHING
|
||||
• Internal storage, SD cards and USB drives, side by side
|
||||
• List or grid view, with sorting by name, size, date or type
|
||||
• Breadcrumb navigation and an in-folder search
|
||||
• Hidden files on request
|
||||
|
||||
MANAGE YOUR FILES
|
||||
• Copy, move, rename and delete, one item or many at once
|
||||
• Create folders and edit text files in the built-in editor
|
||||
• See the details of anything: size, type, location, modified date
|
||||
• Open a file in any app, or share it
|
||||
|
||||
FIND OUT WHERE THE SPACE WENT
|
||||
• A storage-usage view that measures every folder and ranks it largest first
|
||||
• Spot the folder that is actually eating your storage, then act on it
|
||||
|
||||
ARCHIVES
|
||||
• Create ZIP archives, and extract ZIP, TAR, TAR.GZ, GZ, BZ2 and XZ
|
||||
• RAR and 7z are not supported — they need native code this app does not ship, and it says
|
||||
so rather than failing with a strange error
|
||||
|
||||
PRIVATE BY DEFAULT
|
||||
• No INTERNET permission. The app cannot send your data anywhere, because it has no way to.
|
||||
• No analytics, no advertising SDKs, no third-party trackers
|
||||
• No account, no sign-in, nothing to remember
|
||||
• Your files stay on your device, always
|
||||
|
||||
SOLD ONCE
|
||||
• One purchase, every feature, no ads and nothing to buy inside the app
|
||||
• No subscription
|
||||
|
||||
PERMISSIONS, EXPLAINED
|
||||
Plain Files asks for "All files access" because that is what browsing and managing files
|
||||
anywhere on a device requires — the app's core purpose. Google Play only grants this
|
||||
permission to app types that genuinely need it, and a file manager is one of them. If you
|
||||
would rather not grant it, the app still works: it can browse a folder you pick yourself
|
||||
through Android's own folder picker, and it remembers that choice.
|
||||
|
||||
Read the privacy policy: https://example.invalid/plain-files/privacy
|
||||
```
|
||||
|
||||
> Replace the privacy-policy URL with the real public URL before publishing — a repository
|
||||
> file is not acceptable to Play. [PRIVACY.md](../PRIVACY.md) is the source text.
|
||||
|
||||
### Why the permission sentence matters
|
||||
|
||||
Play requires that an app's **core functionality be prominently documented and promoted in
|
||||
the app's description** when it declares `MANAGE_EXTERNAL_STORAGE`. The "PERMISSIONS,
|
||||
EXPLAINED" section is not marketing copy — it is the evidence the Permissions Declaration
|
||||
Form asks for.
|
||||
|
||||
## Still to do before publishing
|
||||
|
||||
- **Screenshots.** `tools/make-store-screenshots.sh` is written and the raw-screenshot
|
||||
directories are in place, but no captures have been taken yet, so
|
||||
`store-assets/screenshots/` is empty. Capture against a **synthetic** library — never a
|
||||
real one, since published screenshots would expose real file names.
|
||||
- **Tablet screenshots.** Play has a 7-inch and a 10-inch slot, each wanting 4+.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 307 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Generates the Google Play store graphics from the app's own icon vectors.
|
||||
#
|
||||
# ./tools/make-store-assets.sh [output-dir] # default: ./store-assets
|
||||
#
|
||||
# Produces, matching Play's asset specs:
|
||||
# icon-512.png 512 x 512 32-bit PNG *with* alpha (ours is full-bleed,
|
||||
# so every pixel ends up opaque anyway)
|
||||
# feature-graphic-1024x500.png 1024 x 500 24-bit PNG, no alpha
|
||||
#
|
||||
# Output lands in ./store-assets/ *inside* the repo, never in $HOME: the editable sources are
|
||||
# the SVGs in tools/store-assets/ and these PNGs are the files that get uploaded to the Play
|
||||
# Console, so they belong with the release they were made for. Pass a directory to override.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
repo="$(dirname "$here")"
|
||||
out="${1:-$repo/store-assets}"
|
||||
mkdir -p "$out"
|
||||
|
||||
render() { # <svg> <width> <height> <filename> <keep-alpha|flatten>
|
||||
local svg="$1" width="$2" height="$3" name="$4" mode="$5"
|
||||
inkscape "$svg" \
|
||||
--export-type=png --export-width="$width" --export-height="$height" \
|
||||
--export-filename="$out/$name" >/dev/null 2>&1
|
||||
if [ "$mode" = keep-alpha ]; then
|
||||
# Play asks for a 32-bit PNG here. The SVG's background rect covers the whole
|
||||
# canvas, so the alpha channel is fully opaque — no accidental transparency.
|
||||
magick "$out/$name" -alpha on PNG32:"$out/$name"
|
||||
else
|
||||
magick "$out/$name" -background '#1565C0' -alpha remove -alpha off PNG24:"$out/$name"
|
||||
fi
|
||||
printf ' %s ' "$out/$name"
|
||||
magick identify -format '%wx%h %[channels] %b\n' "$out/$name"
|
||||
}
|
||||
|
||||
echo "Writing store assets to $out"
|
||||
render "$here/store-assets/icon.svg" 512 512 icon-512.png keep-alpha
|
||||
render "$here/store-assets/feature-graphic.svg" 1024 500 feature-graphic-1024x500.png flatten
|
||||
echo "Done. Screenshots: ./tools/make-store-screenshots.sh # reads store-assets/raw-screenshots/phone/"
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Normalises `adb exec-out screencap` captures into the files that get uploaded to Play.
|
||||
#
|
||||
# ./tools/make-store-screenshots.sh # phone -> 1080x2160
|
||||
# FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh # 7-inch -> 1200x1920
|
||||
# FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh # 10-inch -> 1600x2560
|
||||
#
|
||||
# store-assets/raw-screenshots/<form>/ the raw captures (git-ignored)
|
||||
# store-assets/screenshots/<form>/ what gets uploaded (committed)
|
||||
#
|
||||
# Play's rule that actually bites: JPEG or 24-bit PNG **without alpha**, shorter side >= 320,
|
||||
# longer side <= 3840, and **longer side <= 2x the shorter side**. A phone at its native
|
||||
# 1080x2400 is 2.22:1 and is therefore REJECTED, which is why the phone path crops system
|
||||
# chrome (status + navigation bars) before clamping to 2:1. A tablet frame is already inside
|
||||
# the 2:1 cap on every profile the SDK ships, so its crop defaults to 0 and is opt-in.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
repo="$(dirname "$here")"
|
||||
form="${FORM_FACTOR:-phone}"
|
||||
|
||||
case "$form" in
|
||||
phone) target_w=1080; target_h=2160; default_status=93; default_nav=130 ;;
|
||||
tablet-7in) target_w=1200; target_h=1920; default_status=0; default_nav=0 ;;
|
||||
tablet-10in) target_w=1600; target_h=2560; default_status=0; default_nav=0 ;;
|
||||
*) echo "Unknown FORM_FACTOR '$form' (phone|tablet-7in|tablet-10in)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
status_px="${STATUS_BAR_PX:-$default_status}"
|
||||
nav_px="${NAV_BAR_PX:-$default_nav}"
|
||||
|
||||
raw="$repo/store-assets/raw-screenshots/$form"
|
||||
out="$repo/store-assets/screenshots/$form"
|
||||
mkdir -p "$out"
|
||||
|
||||
shopt -s nullglob
|
||||
captures=("$raw"/*.png)
|
||||
if [ ${#captures[@]} -eq 0 ]; then
|
||||
echo "No captures in $raw — take some with:" >&2
|
||||
echo " adb exec-out screencap -p > $raw/01-roots.png" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Normalising ${#captures[@]} capture(s) from $raw -> $out"
|
||||
for capture in "${captures[@]}"; do
|
||||
name="$(basename "$capture")"
|
||||
magick "$capture" \
|
||||
-gravity North -chop 0x"$status_px" \
|
||||
-gravity South -chop 0x"$nav_px" \
|
||||
+repage \
|
||||
-resize "${target_w}x${target_h}^" \
|
||||
-gravity Center -extent "${target_w}x${target_h}" \
|
||||
-alpha remove -alpha off PNG24:"$out/$name"
|
||||
printf ' %s ' "$out/$name"
|
||||
magick identify -format '%wx%h %[channels] %b\n' "$out/$name"
|
||||
done
|
||||
|
||||
echo "Done."
|
||||
@@ -0,0 +1,43 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="500" viewBox="0 0 1024 500">
|
||||
<!--
|
||||
Play feature graphic (1024 x 500, no transparency).
|
||||
|
||||
Same palette as the app icon (#1565C0) with the icon motif on the left and the store copy
|
||||
on the right. Text stays inside the middle band because Play crops this graphic to a
|
||||
square for some placements, and the outer edges are what get cut.
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#0D3F7A" />
|
||||
<stop offset="0.55" stop-color="#1565C0" />
|
||||
<stop offset="1" stop-color="#2E7DD1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="1024" height="500" fill="url(#bg)" />
|
||||
|
||||
<!--
|
||||
The icon motif, on the left. The icon's own content spans roughly x 24..84 and y 38..77,
|
||||
so it is centred on (54, 57.5).
|
||||
-->
|
||||
<g transform="translate(160,250) scale(3.4) translate(-54,-57.5)">
|
||||
<path d="M34,38 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
|
||||
fill="#FFFFFF" fill-opacity="0.45" />
|
||||
<path d="M28,44 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
|
||||
fill="#FFFFFF" />
|
||||
<path d="M44,56 h20 a2,2 0 0 1 2,2 v12 a2,2 0 0 1 -2,2 h-20 a2,2 0 0 1 -2,-2 v-12 a2,2 0 0 1 2,-2 z"
|
||||
fill="#1565C0" />
|
||||
</g>
|
||||
|
||||
<!--
|
||||
Store copy. Everything here stays inside x 262..762 — the centre square Play crops to for
|
||||
some placements — so the name and the tagline survive the crop even though the motif does
|
||||
not. Sizes were chosen by measuring the strings, not by eye.
|
||||
-->
|
||||
<text x="300" y="215" font-family="DejaVu Sans" font-weight="bold" font-size="50"
|
||||
fill="#FFFFFF">Plain Files</text>
|
||||
<text x="302" y="265" font-family="DejaVu Sans" font-size="20"
|
||||
fill="#FFFFFF" fill-opacity="0.92">Browse and manage your files</text>
|
||||
<text x="302" y="298" font-family="DejaVu Sans" font-size="20"
|
||||
fill="#FFFFFF" fill-opacity="0.92">Ad-free, offline, no account. Pay once.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 108 108">
|
||||
<!--
|
||||
Play app icon (512 x 512, 32-bit PNG with alpha).
|
||||
|
||||
This mirrors the app's own launcher icon exactly: `ic_launcher_background.xml` plus the
|
||||
three paths from `ic_launcher_foreground.xml`. Background, paths and opacities are
|
||||
identical, so the store icon *is* the launcher icon rather than a lookalike that drifts
|
||||
after the next rebrand.
|
||||
|
||||
The blue rect covers the whole 108 dp canvas, which is what makes the PNG full bleed:
|
||||
Play's mask can crop to any shape without revealing a gap.
|
||||
-->
|
||||
<rect x="0" y="0" width="108" height="108" fill="#1565C0" />
|
||||
|
||||
<!-- Back folder, offset up and to the right -->
|
||||
<path d="M34,38 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
|
||||
fill="#FFFFFF" fill-opacity="0.45" />
|
||||
|
||||
<!-- Front folder -->
|
||||
<path d="M28,44 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
|
||||
fill="#FFFFFF" />
|
||||
|
||||
<!-- Document tab -->
|
||||
<path d="M44,56 h20 a2,2 0 0 1 2,2 v12 a2,2 0 0 1 -2,2 h-20 a2,2 0 0 1 -2,-2 v-12 a2,2 0 0 1 2,-2 z"
|
||||
fill="#1565C0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user