mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
merged battery module
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
9A09C89E22B3A7C90018426F /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89D22B3A7C90018426F /* Battery.swift */; };
|
||||
9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89F22B3A7E20018426F /* BatteryReader.swift */; };
|
||||
9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C8A122B3D94D0018426F /* BatteryView.swift */; };
|
||||
9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1410F8229E721100D29793 /* AppDelegate.swift */; };
|
||||
9A141100229E721200D29793 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A1410FE229E721200D29793 /* Main.storyboard */; };
|
||||
9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A18422A1D26D0033E318 /* MenuBar.swift */; };
|
||||
@@ -44,6 +47,9 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
9A09C89D22B3A7C90018426F /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = "<group>"; };
|
||||
9A09C89F22B3A7E20018426F /* BatteryReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryReader.swift; sourceTree = "<group>"; };
|
||||
9A09C8A122B3D94D0018426F /* BatteryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryView.swift; sourceTree = "<group>"; };
|
||||
9A1410F5229E721100D29793 /* Stats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stats.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9A1410F8229E721100D29793 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
9A1410FF229E721200D29793 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
@@ -91,6 +97,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
9A09C89C22B3A7BB0018426F /* Battery */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9A09C89D22B3A7C90018426F /* Battery.swift */,
|
||||
9A09C89F22B3A7E20018426F /* BatteryReader.swift */,
|
||||
);
|
||||
path = Battery;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9A1410EC229E721100D29793 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -138,6 +153,7 @@
|
||||
9A5B1CBA229E7892008B9D3C /* Modules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9A09C89C22B3A7BB0018426F /* Battery */,
|
||||
9A7B8F5C22A2926500DEB352 /* CPU */,
|
||||
9A7B8F6222A2C17000DEB352 /* Memory */,
|
||||
9A7B8F6322A2C17500DEB352 /* Disk */,
|
||||
@@ -158,6 +174,7 @@
|
||||
9A74D59522B440D4004FE1FA /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9A09C8A122B3D94D0018426F /* BatteryView.swift */,
|
||||
9A74D59322B4315C004FE1FA /* Chart.swift */,
|
||||
9A74D59622B44498004FE1FA /* Mini.swift */,
|
||||
);
|
||||
@@ -314,13 +331,16 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */,
|
||||
9A7B8F6F22A2C57000DEB352 /* DiskReader.swift in Sources */,
|
||||
9A7B8F6922A2C3A100DEB352 /* Memory.swift in Sources */,
|
||||
9A7B8F5E22A2A57600DEB352 /* CPUReader.swift in Sources */,
|
||||
9A74D59422B4315C004FE1FA /* Chart.swift in Sources */,
|
||||
9A09C89E22B3A7C90018426F /* Battery.swift in Sources */,
|
||||
9A7B8F6D22A2C3D600DEB352 /* MemoryReader.swift in Sources */,
|
||||
9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */,
|
||||
9A57A19D22A1E3270033E318 /* CPU.swift in Sources */,
|
||||
9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */,
|
||||
9A57A19B22A1E1C50033E318 /* Module.swift in Sources */,
|
||||
9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */,
|
||||
9A7B8F6B22A2C3A700DEB352 /* Disk.swift in Sources */,
|
||||
|
||||
@@ -13,7 +13,7 @@ extension Notification.Name {
|
||||
static let killLauncher = Notification.Name("killLauncher")
|
||||
}
|
||||
|
||||
let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk()])
|
||||
let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk(), Battery()])
|
||||
let colors: Observable<Bool> = Observable(true)
|
||||
|
||||
@NSApplicationMain
|
||||
|
||||
@@ -37,6 +37,11 @@ class MenuBar {
|
||||
self.menuBarItem.menu?.removeAllItems()
|
||||
self.menuBarItem.menu = self.buildMenu()
|
||||
}
|
||||
module.available.subscribe(observer: self) { (value, _) in
|
||||
self.buildModulesView()
|
||||
self.menuBarItem.menu?.removeAllItems()
|
||||
self.menuBarItem.menu = self.buildMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +49,9 @@ class MenuBar {
|
||||
let menu = NSMenu()
|
||||
|
||||
for module in modules.value {
|
||||
menu.addItem(module.menu)
|
||||
if module.available.value {
|
||||
menu.addItem(module.menu)
|
||||
}
|
||||
}
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
@@ -115,7 +122,7 @@ class MenuBar {
|
||||
|
||||
WIDTH = 0
|
||||
for module in modules.value {
|
||||
if module.active.value {
|
||||
if module.active.value && module.available.value {
|
||||
module.start()
|
||||
WIDTH = WIDTH + module.view.frame.size.width
|
||||
stack.addView(module.view, in: NSStackView.Gravity.center)
|
||||
|
||||
72
Stats/Modules/Battery/Battery.swift
Normal file
72
Stats/Modules/Battery/Battery.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Battery.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/06/2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Battery: Module {
|
||||
let name: String = "Battery"
|
||||
let shortName: String = ""
|
||||
var view: NSView = NSView()
|
||||
var menu: NSMenuItem = NSMenuItem()
|
||||
var submenu: NSMenu = NSMenu()
|
||||
var active: Observable<Bool>
|
||||
var available: Observable<Bool>
|
||||
var reader: Reader = BatteryReader()
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
var widgetType: WidgetType = Widgets.Mini
|
||||
|
||||
init() {
|
||||
self.available = Observable(self.reader.available)
|
||||
self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true)
|
||||
self.view = BatteryView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT))
|
||||
initMenu()
|
||||
}
|
||||
|
||||
func start() {
|
||||
if !self.reader.usage.value.isNaN {
|
||||
let value = self.reader.usage!.value
|
||||
(self.view as! BatteryView).setCharging(value: value > 0)
|
||||
(self.view as! Widget).value(value: abs(value))
|
||||
}
|
||||
|
||||
self.reader.start()
|
||||
self.reader.usage.subscribe(observer: self) { (value, _) in
|
||||
if !value.isNaN {
|
||||
(self.view as! BatteryView).setCharging(value: value > 0)
|
||||
(self.view as! Widget).value(value: abs(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initMenu() {
|
||||
menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "")
|
||||
if defaults.object(forKey: name) != nil {
|
||||
menu.state = defaults.bool(forKey: name) ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
} else {
|
||||
menu.state = NSControl.StateValue.on
|
||||
}
|
||||
menu.target = self
|
||||
menu.isEnabled = true
|
||||
}
|
||||
|
||||
@objc func toggle(_ sender: NSMenuItem) {
|
||||
let state = sender.state != NSControl.StateValue.on
|
||||
|
||||
sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on
|
||||
self.defaults.set(state, forKey: name)
|
||||
self.active << state
|
||||
|
||||
if !state {
|
||||
self.stop()
|
||||
} else {
|
||||
self.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
Stats/Modules/Battery/BatteryReader.swift
Normal file
55
Stats/Modules/Battery/BatteryReader.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// BatteryReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/06/2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import IOKit.ps
|
||||
|
||||
class BatteryReader: Reader {
|
||||
var usage: Observable<Float>!
|
||||
var available: Bool = false
|
||||
var updateTimer: Timer!
|
||||
|
||||
init() {
|
||||
self.usage = Observable(0)
|
||||
read()
|
||||
}
|
||||
|
||||
func start() {
|
||||
if updateTimer != nil {
|
||||
return
|
||||
}
|
||||
updateTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(read), userInfo: nil, repeats: true)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
if updateTimer == nil {
|
||||
return
|
||||
}
|
||||
updateTimer.invalidate()
|
||||
updateTimer = nil
|
||||
}
|
||||
|
||||
@objc func read() {
|
||||
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
||||
self.available = psList.count != 0
|
||||
|
||||
for ps in psList {
|
||||
if let psDesc = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? [String: Any] {
|
||||
let isCharging = (psDesc[kIOPSIsChargingKey] as? Bool)
|
||||
var cap: Float = Float(psDesc[kIOPSCurrentCapacityKey] as! Int) / 100
|
||||
|
||||
if !isCharging! {
|
||||
cap = 0 - cap
|
||||
}
|
||||
|
||||
self.usage << Float(cap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,14 @@ class CPU: Module {
|
||||
var menu: NSMenuItem = NSMenuItem()
|
||||
var submenu: NSMenu = NSMenu()
|
||||
var active: Observable<Bool>
|
||||
var available: Observable<Bool>
|
||||
var reader: Reader = CPUReader()
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
var widgetType: WidgetType
|
||||
|
||||
init() {
|
||||
self.available = Observable(true)
|
||||
self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true)
|
||||
self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini
|
||||
initMenu()
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
|
||||
class CPUReader: Reader {
|
||||
var usage: Observable<Float>!
|
||||
var available: Bool = true
|
||||
var cpuInfo: processor_info_array_t!
|
||||
var prevCpuInfo: processor_info_array_t?
|
||||
var numCpuInfo: mach_msg_type_number_t = 0
|
||||
|
||||
@@ -17,11 +17,13 @@ class Disk: Module {
|
||||
var widgetType: WidgetType
|
||||
|
||||
var active: Observable<Bool>
|
||||
var available: Observable<Bool>
|
||||
var reader: Reader = DiskReader()
|
||||
|
||||
@IBOutlet weak var value: NSTextField!
|
||||
|
||||
init() {
|
||||
self.available = Observable(true)
|
||||
self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true)
|
||||
self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
|
||||
class DiskReader: Reader {
|
||||
var usage: Observable<Float>!
|
||||
var available: Bool = true
|
||||
var updateTimer: Timer!
|
||||
|
||||
init() {
|
||||
|
||||
@@ -15,6 +15,7 @@ class Memory: Module {
|
||||
var menu: NSMenuItem = NSMenuItem()
|
||||
var submenu: NSMenu = NSMenu()
|
||||
var active: Observable<Bool>
|
||||
var available: Observable<Bool>
|
||||
var reader: Reader = MemoryReader()
|
||||
var widgetType: WidgetType
|
||||
|
||||
@@ -23,6 +24,7 @@ class Memory: Module {
|
||||
@IBOutlet weak var value: NSTextField!
|
||||
|
||||
init() {
|
||||
self.available = Observable(true)
|
||||
self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true)
|
||||
self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini
|
||||
initMenu()
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
|
||||
class MemoryReader: Reader {
|
||||
var usage: Observable<Float>!
|
||||
var available: Bool = true
|
||||
var updateTimer: Timer!
|
||||
var totalSize: Float
|
||||
|
||||
|
||||
91
Stats/Widgets/BatteryView.swift
Normal file
91
Stats/Widgets/BatteryView.swift
Normal file
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// BatteryView.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/06/2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class BatteryView: NSView, Widget {
|
||||
var value: Float {
|
||||
didSet {
|
||||
self.redraw()
|
||||
}
|
||||
}
|
||||
var charging: Bool {
|
||||
didSet {
|
||||
self.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: NSRect) {
|
||||
self.value = 1.0
|
||||
self.charging = false
|
||||
super.init(frame: frame)
|
||||
self.wantsLayer = true
|
||||
self.addSubview(NSView())
|
||||
}
|
||||
|
||||
required init?(coder decoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
let x: CGFloat = 4.0
|
||||
let w: CGFloat = dirtyRect.size.width - (x * 2)
|
||||
let h: CGFloat = 11.0
|
||||
let y: CGFloat = (dirtyRect.size.height - h) / 2
|
||||
let r: CGFloat = 1.0
|
||||
|
||||
let battery = NSBezierPath(roundedRect: NSRect(x: x-1, y: y, width: w-1, height: h), xRadius: r, yRadius: r)
|
||||
|
||||
let bPX: CGFloat = x+w-2
|
||||
let bPY: CGFloat = (dirtyRect.size.height / 2) - 2
|
||||
let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX, y: bPY, width: 2, height: 4), xRadius: r, yRadius: r)
|
||||
if self.charging {
|
||||
NSColor.systemGreen.set()
|
||||
} else {
|
||||
NSColor.labelColor.set()
|
||||
}
|
||||
batteryPoint.lineWidth = 1.1
|
||||
batteryPoint.stroke()
|
||||
batteryPoint.fill()
|
||||
|
||||
let maxWidth = w-4.25
|
||||
let inner = NSBezierPath(roundedRect: NSRect(x: x+0.75, y: y+1.5, width: maxWidth*CGFloat(self.value), height: h-3), xRadius: 0.5, yRadius: 0.5)
|
||||
self.value.batteryColor().set()
|
||||
inner.lineWidth = 0
|
||||
inner.stroke()
|
||||
inner.close()
|
||||
inner.fill()
|
||||
|
||||
if self.charging {
|
||||
NSColor.systemGreen.set()
|
||||
} else {
|
||||
NSColor.labelColor.set()
|
||||
}
|
||||
battery.lineWidth = 0.8
|
||||
battery.stroke()
|
||||
}
|
||||
|
||||
func redraw() {
|
||||
self.needsDisplay = true
|
||||
setNeedsDisplay(self.frame)
|
||||
}
|
||||
|
||||
func value(value: Float) {
|
||||
if self.value != value {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
func setCharging(value: Bool) {
|
||||
if self.charging != value {
|
||||
self.charging = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,7 @@ class Chart: NSView, Widget {
|
||||
var height: CGFloat = 0.0
|
||||
var points: [Float] {
|
||||
didSet {
|
||||
self.needsDisplay = true
|
||||
setNeedsDisplay(self.frame)
|
||||
self.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +78,11 @@ class Chart: NSView, Widget {
|
||||
graphPath.stroke()
|
||||
}
|
||||
|
||||
func redraw() {
|
||||
self.needsDisplay = true
|
||||
setNeedsDisplay(self.frame)
|
||||
}
|
||||
|
||||
func value(value: Float) {
|
||||
if self.points.count < 50 {
|
||||
self.points.append(value)
|
||||
|
||||
@@ -65,6 +65,12 @@ class Mini: NSView, Widget {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func redraw() {
|
||||
self.valueView.textColor = Float(self.value).usageColor()
|
||||
self.needsDisplay = true
|
||||
setNeedsDisplay(self.frame)
|
||||
}
|
||||
|
||||
func value(value: Float) {
|
||||
if self.value != value {
|
||||
self.value = value
|
||||
|
||||
@@ -14,18 +14,46 @@ extension Float {
|
||||
return NSString(format: "%.\(decimalPlaces)f" as NSString, self) as String
|
||||
}
|
||||
|
||||
func usageColor() -> NSColor {
|
||||
func usageColor(reversed: Bool = false) -> NSColor {
|
||||
if !colors.value {
|
||||
return NSColor.textColor
|
||||
}
|
||||
|
||||
if reversed {
|
||||
switch self {
|
||||
case 0.6...0.8:
|
||||
return NSColor.systemOrange
|
||||
case 0.8...1:
|
||||
return NSColor.systemGreen
|
||||
default:
|
||||
return NSColor.systemRed
|
||||
}
|
||||
} else {
|
||||
switch self {
|
||||
case 0.6...0.8:
|
||||
return NSColor.systemOrange
|
||||
case 0.8...1:
|
||||
return NSColor.systemRed
|
||||
default:
|
||||
return NSColor.systemGreen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func batteryColor() -> NSColor {
|
||||
switch self {
|
||||
case 0.6...0.8:
|
||||
case 0.2...0.4:
|
||||
if !colors.value {
|
||||
return NSColor.controlTextColor
|
||||
}
|
||||
return NSColor.systemOrange
|
||||
case 0.8...1:
|
||||
return NSColor.systemRed
|
||||
default:
|
||||
case 0.4...1:
|
||||
if !colors.value {
|
||||
return NSColor.controlTextColor
|
||||
}
|
||||
return NSColor.systemGreen
|
||||
default:
|
||||
return NSColor.systemRed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ protocol Module: class {
|
||||
var view: NSView { get set }
|
||||
var menu: NSMenuItem { get }
|
||||
var active: Observable<Bool> { get }
|
||||
var available: Observable<Bool> { get }
|
||||
var reader: Reader { get }
|
||||
var widgetType: WidgetType { get }
|
||||
|
||||
@@ -53,7 +54,7 @@ extension Module {
|
||||
}
|
||||
|
||||
self.reader.start()
|
||||
self.reader.usage.subscribe(observer: self as AnyObject) { (value, _) in
|
||||
self.reader.usage.subscribe(observer: self) { (value, _) in
|
||||
if !value.isNaN {
|
||||
guard let widget = self.view as? Widget else {
|
||||
return
|
||||
@@ -61,23 +62,34 @@ extension Module {
|
||||
widget.value(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
colors.subscribe(observer: self) { (value, _) in
|
||||
guard let widget = self.view as? Widget else {
|
||||
return
|
||||
}
|
||||
widget.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
self.reader.stop()
|
||||
self.reader.usage.unsubscribe(observer: self as AnyObject)
|
||||
self.reader.usage.unsubscribe(observer: self)
|
||||
colors.unsubscribe(observer: self)
|
||||
}
|
||||
}
|
||||
|
||||
protocol Reader {
|
||||
var usage: Observable<Float>! { get }
|
||||
var available: Bool { get }
|
||||
var updateTimer: Timer! { get set }
|
||||
func start()
|
||||
func read()
|
||||
func stop()
|
||||
func read()
|
||||
}
|
||||
|
||||
protocol Widget {
|
||||
func value(value: Float)
|
||||
func redraw()
|
||||
}
|
||||
|
||||
typealias WidgetType = Float
|
||||
|
||||
Reference in New Issue
Block a user