mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-03 00:58:26 -04:00
feat(tracker): change keyboard shortcuts to mimic old trackers
This commit is contained in:
parent
b4a63ce362
commit
91b7850bf7
@ -28,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Empty patch should not crash the native synth ([#148][i148])
|
- Empty patch should not crash the native synth ([#148][i148])
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- The keyboard shortcuts are now again closer to what they were old trackers
|
||||||
|
([#151][i151])
|
||||||
- The stand-alone apps now output floating point sound, as made possible by
|
- The stand-alone apps now output floating point sound, as made possible by
|
||||||
upgrading oto-library to latest version. This way the tracker sound output
|
upgrading oto-library to latest version. This way the tracker sound output
|
||||||
matches the compiled output better, as usually compiled intros output sound in
|
matches the compiled output better, as usually compiled intros output sound in
|
||||||
@ -237,6 +239,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[i148]: https://github.com/vsariola/sointu/issues/148
|
[i148]: https://github.com/vsariola/sointu/issues/148
|
||||||
[i149]: https://github.com/vsariola/sointu/issues/149
|
[i149]: https://github.com/vsariola/sointu/issues/149
|
||||||
[i150]: https://github.com/vsariola/sointu/issues/150
|
[i150]: https://github.com/vsariola/sointu/issues/150
|
||||||
|
[i151]: https://github.com/vsariola/sointu/issues/151
|
||||||
[i154]: https://github.com/vsariola/sointu/issues/154
|
[i154]: https://github.com/vsariola/sointu/issues/154
|
||||||
[i158]: https://github.com/vsariola/sointu/issues/158
|
[i158]: https://github.com/vsariola/sointu/issues/158
|
||||||
[i162]: https://github.com/vsariola/sointu/issues/162
|
[i162]: https://github.com/vsariola/sointu/issues/162
|
||||||
|
@ -275,18 +275,75 @@ func (m *Model) RemoveUnused() Action {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Rewind() Action {
|
func (m *Model) PlayFromCurrentPosition() Action {
|
||||||
return Action{
|
return Action{
|
||||||
allowed: func() bool {
|
allowed: func() bool { return !m.instrEnlarged },
|
||||||
return m.playing || !m.instrEnlarged
|
|
||||||
},
|
|
||||||
do: func() {
|
do: func() {
|
||||||
|
m.setPanic(false)
|
||||||
|
m.setLoop(Loop{})
|
||||||
|
m.playing = true
|
||||||
|
m.send(StartPlayMsg{m.d.Cursor.SongPos})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) PlayFromSongStart() Action {
|
||||||
|
return Action{
|
||||||
|
allowed: func() bool { return !m.instrEnlarged },
|
||||||
|
do: func() {
|
||||||
|
m.setPanic(false)
|
||||||
|
m.setLoop(Loop{})
|
||||||
m.playing = true
|
m.playing = true
|
||||||
m.send(StartPlayMsg{})
|
m.send(StartPlayMsg{})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) PlaySelected() Action {
|
||||||
|
return Action{
|
||||||
|
allowed: func() bool { return !m.instrEnlarged },
|
||||||
|
do: func() {
|
||||||
|
m.setPanic(false)
|
||||||
|
m.playing = true
|
||||||
|
l := m.OrderRows().List()
|
||||||
|
a, b := l.listRange()
|
||||||
|
newLoop := Loop{a, b - a + 1}
|
||||||
|
m.setLoop(newLoop)
|
||||||
|
m.send(StartPlayMsg{sointu.SongPos{OrderRow: a, PatternRow: 0}})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) PlayFromLoopStart() Action {
|
||||||
|
return Action{
|
||||||
|
allowed: func() bool { return !m.instrEnlarged },
|
||||||
|
do: func() {
|
||||||
|
m.setPanic(false)
|
||||||
|
if m.loop == (Loop{}) {
|
||||||
|
m.PlaySelected().Do()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.playing = true
|
||||||
|
m.send(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) StopPlaying() Action {
|
||||||
|
return Action{
|
||||||
|
allowed: func() bool { return true },
|
||||||
|
do: func() {
|
||||||
|
if !m.playing {
|
||||||
|
m.setPanic(true)
|
||||||
|
m.setLoop(Loop{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.playing = false
|
||||||
|
(*Model)(m).send(IsPlayingMsg{false})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) AddOrderRow(before bool) Action {
|
func (m *Model) AddOrderRow(before bool) Action {
|
||||||
return Allow(func() {
|
return Allow(func() {
|
||||||
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
||||||
@ -394,8 +451,9 @@ func (m *Model) completeAction(checkSave bool) {
|
|||||||
}
|
}
|
||||||
switch m.dialog {
|
switch m.dialog {
|
||||||
case NewSongChanges, NewSongSaveExplorer:
|
case NewSongChanges, NewSongSaveExplorer:
|
||||||
c := m.change("NewSong", SongChange|LoopChange, MajorChange)
|
c := m.change("NewSong", SongChange, MajorChange)
|
||||||
m.resetSong()
|
m.resetSong()
|
||||||
|
m.setLoop(Loop{})
|
||||||
c()
|
c()
|
||||||
m.d.ChangedSinceSave = false
|
m.d.ChangedSinceSave = false
|
||||||
m.dialog = NoDialog
|
m.dialog = NoDialog
|
||||||
@ -408,3 +466,17 @@ func (m *Model) completeAction(checkSave bool) {
|
|||||||
m.dialog = NoDialog
|
m.dialog = NoDialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) setPanic(val bool) {
|
||||||
|
if m.panic != val {
|
||||||
|
m.panic = val
|
||||||
|
m.send(PanicMsg{val})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) setLoop(newLoop Loop) {
|
||||||
|
if m.loop != newLoop {
|
||||||
|
m.loop = newLoop
|
||||||
|
m.send(newLoop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -51,8 +51,7 @@ func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
|
|||||||
func (m *Panic) Bool() Bool { return Bool{m} }
|
func (m *Panic) Bool() Bool { return Bool{m} }
|
||||||
func (m *Panic) Value() bool { return m.panic }
|
func (m *Panic) Value() bool { return m.panic }
|
||||||
func (m *Panic) setValue(val bool) {
|
func (m *Panic) setValue(val bool) {
|
||||||
m.panic = val
|
(*Model)(m).setPanic(val)
|
||||||
(*Model)(m).send(PanicMsg{val})
|
|
||||||
}
|
}
|
||||||
func (m *Panic) Enabled() bool { return true }
|
func (m *Panic) Enabled() bool { return true }
|
||||||
|
|
||||||
@ -74,6 +73,7 @@ func (m *Playing) Value() bool { return m.playing }
|
|||||||
func (m *Playing) setValue(val bool) {
|
func (m *Playing) setValue(val bool) {
|
||||||
m.playing = val
|
m.playing = val
|
||||||
if m.playing {
|
if m.playing {
|
||||||
|
(*Model)(m).setPanic(false)
|
||||||
(*Model)(m).send(StartPlayMsg{m.d.Cursor.SongPos})
|
(*Model)(m).send(StartPlayMsg{m.d.Cursor.SongPos})
|
||||||
} else {
|
} else {
|
||||||
(*Model)(m).send(IsPlayingMsg{val})
|
(*Model)(m).send(IsPlayingMsg{val})
|
||||||
@ -98,9 +98,9 @@ func (m *CommentExpanded) Enabled() bool { return true }
|
|||||||
// NoteTracking methods
|
// NoteTracking methods
|
||||||
|
|
||||||
func (m *NoteTracking) Bool() Bool { return Bool{m} }
|
func (m *NoteTracking) Bool() Bool { return Bool{m} }
|
||||||
func (m *NoteTracking) Value() bool { return m.playing && m.noteTracking }
|
func (m *NoteTracking) Value() bool { return m.noteTracking }
|
||||||
func (m *NoteTracking) setValue(val bool) { m.noteTracking = val }
|
func (m *NoteTracking) setValue(val bool) { m.noteTracking = val }
|
||||||
func (m *NoteTracking) Enabled() bool { return m.playing }
|
func (m *NoteTracking) Enabled() bool { return true }
|
||||||
|
|
||||||
// Effect methods
|
// Effect methods
|
||||||
|
|
||||||
@ -173,16 +173,15 @@ func (m *UnitDisabled) Enabled() bool {
|
|||||||
// LoopToggle methods
|
// LoopToggle methods
|
||||||
|
|
||||||
func (m *LoopToggle) Bool() Bool { return Bool{m} }
|
func (m *LoopToggle) Bool() Bool { return Bool{m} }
|
||||||
func (m *LoopToggle) Value() bool { return m.d.Loop.Length > 0 }
|
func (m *LoopToggle) Value() bool { return m.loop.Length > 0 }
|
||||||
func (t *LoopToggle) setValue(val bool) {
|
func (t *LoopToggle) setValue(val bool) {
|
||||||
m := (*Model)(t)
|
m := (*Model)(t)
|
||||||
defer m.change("SetLoopAction", LoopChange, MinorChange)()
|
newLoop := Loop{}
|
||||||
if !val {
|
if val {
|
||||||
m.d.Loop = Loop{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l := m.OrderRows().List()
|
l := m.OrderRows().List()
|
||||||
a, b := l.listRange()
|
a, b := l.listRange()
|
||||||
m.d.Loop = Loop{a, b - a + 1}
|
newLoop = Loop{a, b - a + 1}
|
||||||
|
}
|
||||||
|
m.setLoop(newLoop)
|
||||||
}
|
}
|
||||||
func (m *LoopToggle) Enabled() bool { return true }
|
func (m *LoopToggle) Enabled() bool { return true }
|
||||||
|
@ -121,19 +121,34 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
case "F3":
|
case "F3":
|
||||||
t.InstrumentEditor.Focus()
|
t.InstrumentEditor.Focus()
|
||||||
return
|
return
|
||||||
case "F5":
|
case "Space":
|
||||||
t.SongPanel.RewindBtn.Action.Do()
|
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
|
||||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
t.Playing().Bool().Toggle()
|
||||||
return
|
return
|
||||||
case "F6", "Space":
|
case "F5":
|
||||||
t.SongPanel.PlayingBtn.Bool.Toggle()
|
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
|
||||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
if e.Modifiers.Contain(key.ModCtrl) {
|
||||||
|
t.Model.PlayFromSongStart().Do()
|
||||||
|
} else {
|
||||||
|
t.Model.PlayFromCurrentPosition().Do()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case "F6":
|
||||||
|
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
|
||||||
|
if e.Modifiers.Contain(key.ModCtrl) {
|
||||||
|
t.Model.PlayFromLoopStart().Do()
|
||||||
|
} else {
|
||||||
|
t.Model.PlaySelected().Do()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
case "F7":
|
case "F7":
|
||||||
t.SongPanel.RecordBtn.Bool.Toggle()
|
t.IsRecording().Bool().Toggle()
|
||||||
return
|
return
|
||||||
case "F8":
|
case "F8":
|
||||||
t.SongPanel.NoteTracking.Bool.Toggle()
|
t.StopPlaying().Do()
|
||||||
|
return
|
||||||
|
case "F9":
|
||||||
|
t.NoteTracking().Bool().Toggle()
|
||||||
return
|
return
|
||||||
case "F12":
|
case "F12":
|
||||||
t.Panic().Bool().Toggle()
|
t.Panic().Bool().Toggle()
|
||||||
|
@ -56,7 +56,7 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
|
|||||||
RecordBtn: NewBoolClickable(model.IsRecording().Bool()),
|
RecordBtn: NewBoolClickable(model.IsRecording().Bool()),
|
||||||
NoteTracking: NewBoolClickable(model.NoteTracking().Bool()),
|
NoteTracking: NewBoolClickable(model.NoteTracking().Bool()),
|
||||||
PlayingBtn: NewBoolClickable(model.Playing().Bool()),
|
PlayingBtn: NewBoolClickable(model.Playing().Bool()),
|
||||||
RewindBtn: NewActionClickable(model.Rewind()),
|
RewindBtn: NewActionClickable(model.PlayFromSongStart()),
|
||||||
}
|
}
|
||||||
ret.fileMenuItems = []MenuItem{
|
ret.fileMenuItems = []MenuItem{
|
||||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()},
|
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()},
|
||||||
@ -105,10 +105,10 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
|
|||||||
in := layout.UniformInset(unit.Dp(1))
|
in := layout.UniformInset(unit.Dp(1))
|
||||||
|
|
||||||
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)")
|
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)")
|
||||||
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(F5)")
|
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(Ctrl+F5)")
|
||||||
playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)")
|
playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F5 / Space)", "Stop (F8)")
|
||||||
recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)")
|
recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)")
|
||||||
noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff\n(F8)", "Follow\nOn\n(F8)")
|
noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff", "Follow\nOn")
|
||||||
loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)")
|
loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)")
|
||||||
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
@ -36,7 +36,6 @@ type (
|
|||||||
ChangedSinceSave bool
|
ChangedSinceSave bool
|
||||||
RecoveryFilePath string
|
RecoveryFilePath string
|
||||||
ChangedSinceRecovery bool
|
ChangedSinceRecovery bool
|
||||||
Loop Loop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Model struct {
|
Model struct {
|
||||||
@ -59,6 +58,7 @@ type (
|
|||||||
recording bool
|
recording bool
|
||||||
playing bool
|
playing bool
|
||||||
playPosition sointu.SongPos
|
playPosition sointu.SongPos
|
||||||
|
loop Loop
|
||||||
noteTracking bool
|
noteTracking bool
|
||||||
quitted bool
|
quitted bool
|
||||||
|
|
||||||
@ -132,7 +132,6 @@ const (
|
|||||||
ScoreChange
|
ScoreChange
|
||||||
BPMChange
|
BPMChange
|
||||||
RowsPerBeatChange
|
RowsPerBeatChange
|
||||||
LoopChange
|
|
||||||
SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange
|
SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -156,7 +155,7 @@ const maxUndo = 64
|
|||||||
func (m *Model) AverageVolume() Volume { return m.avgVolume }
|
func (m *Model) AverageVolume() Volume { return m.avgVolume }
|
||||||
func (m *Model) PeakVolume() Volume { return m.peakVolume }
|
func (m *Model) PeakVolume() Volume { return m.peakVolume }
|
||||||
func (m *Model) PlayPosition() sointu.SongPos { return m.playPosition }
|
func (m *Model) PlayPosition() sointu.SongPos { return m.playPosition }
|
||||||
func (m *Model) Loop() Loop { return m.d.Loop }
|
func (m *Model) Loop() Loop { return m.loop }
|
||||||
func (m *Model) PlaySongRow() int { return m.d.Song.Score.SongRow(m.playPosition) }
|
func (m *Model) PlaySongRow() int { return m.d.Song.Score.SongRow(m.playPosition) }
|
||||||
func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave }
|
func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave }
|
||||||
func (m *Model) Dialog() Dialog { return m.dialog }
|
func (m *Model) Dialog() Dialog { return m.dialog }
|
||||||
@ -183,7 +182,7 @@ func NewModelPlayer(synther sointu.Synther, recoveryFilePath string) (*Model, *P
|
|||||||
modelMsgs: modelMessages,
|
modelMsgs: modelMessages,
|
||||||
synther: synther,
|
synther: synther,
|
||||||
song: m.d.Song.Copy(),
|
song: m.d.Song.Copy(),
|
||||||
loop: m.d.Loop,
|
loop: m.loop,
|
||||||
avgVolumeMeter: VolumeAnalyzer{Attack: 0.3, Release: 0.3, Min: -100, Max: 20},
|
avgVolumeMeter: VolumeAnalyzer{Attack: 0.3, Release: 0.3, Min: -100, Max: 20},
|
||||||
peakVolumeMeter: VolumeAnalyzer{Attack: 1e-4, Release: 1, Min: -100, Max: 20},
|
peakVolumeMeter: VolumeAnalyzer{Attack: 1e-4, Release: 1, Min: -100, Max: 20},
|
||||||
}
|
}
|
||||||
@ -244,9 +243,6 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
|
|||||||
if m.changeType&RowsPerBeatChange != 0 {
|
if m.changeType&RowsPerBeatChange != 0 {
|
||||||
m.send(RowsPerBeatMsg{m.d.Song.RowsPerBeat})
|
m.send(RowsPerBeatMsg{m.d.Song.RowsPerBeat})
|
||||||
}
|
}
|
||||||
if m.changeType&LoopChange != 0 {
|
|
||||||
m.send(m.d.Loop)
|
|
||||||
}
|
|
||||||
m.undoSkipCounter++
|
m.undoSkipCounter++
|
||||||
var limit int
|
var limit int
|
||||||
switch m.changeSeverity {
|
switch m.changeSeverity {
|
||||||
@ -322,7 +318,6 @@ func (m *Model) UnmarshalRecovery(bytes []byte) {
|
|||||||
}
|
}
|
||||||
m.d.ChangedSinceRecovery = false
|
m.d.ChangedSinceRecovery = false
|
||||||
m.send(m.d.Song.Copy())
|
m.send(m.d.Song.Copy())
|
||||||
m.send(m.d.Loop)
|
|
||||||
m.updatePatternUseCount()
|
m.updatePatternUseCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +397,6 @@ func (m *Model) resetSong() {
|
|||||||
}
|
}
|
||||||
m.d.FilePath = ""
|
m.d.FilePath = ""
|
||||||
m.d.ChangedSinceSave = false
|
m.d.ChangedSinceSave = false
|
||||||
m.d.Loop = Loop{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// send sends a message to the player
|
// send sends a message to the player
|
||||||
|
@ -82,7 +82,7 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
|
|||||||
s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed)
|
s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed)
|
||||||
s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed)
|
s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed)
|
||||||
s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed)
|
s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed)
|
||||||
s.IterateAction("Rewind", s.model.Rewind(), yield, seed)
|
s.IterateAction("Rewind", s.model.PlayFromSongStart(), yield, seed)
|
||||||
s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed)
|
s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed)
|
||||||
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
|
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
|
||||||
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)
|
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user